1. 背景
多点登录,访问每个服务都需要重新登陆
2. 概念
登录一个站点后可以免登录其他站点
3. 准备工作
3.1 项目结构
3.2 执行sql脚本
3.3 数据库表结构
3.4 sso-system子工程的依赖
<dependencies>
<!--1.数据库访问相关-->
<!--1.1 mysql 数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--1.2 mybatis plus 插件-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--服务治理相关-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--Web 服务相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.5 sso-system子工程yml文件配置
server:
port: 8061
spring:
application:
name: sso-system #服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 #服务注册发现配置
config:
server-addr: localhost:8848
file-extension: yml
datasource: #连接数据库的配置
url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
logging: #配置日志级别
level:
com.jt: debug
3.6 测试连接
package com.jt;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
public class DataSourceTests {
@Autowired
private DataSource dataSource;//HikariDataSource
@Test
void testGetConnection() throws SQLException {
Connection conn=
dataSource.getConnection();
System.out.println(conn);
}
}
实现效果
4. 业务1
4.1 编辑pojo
package com.jt.system.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@Data
@TableName("tb-users")
public class User implements Serializable {
private static final long serialVersionUID = 4831304712151465443L;
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String status;
}
4.2 编辑Dao层
package com.jt.system.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("select id,username,password,status " +
"from tb_users " +
"where username=#{username}")
User selectUserByUsername(String username);
}
4.3 编辑测试类
package com.jt;
import com.jt.system.pojo.User;
import com.jt.system.dao.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
public class UserMapperTests {
@Autowired
private UserMapper userMapper;
@Test
void testSelectUserByUsername(){
User user =
userMapper.selectUserByUsername("admin");
System.out.println(user);
}
}
实现效果
拓展:享元模式
…
5. 业务2基于用户id查询用户权限
5.1 sql语句
5.2 Mapper层
package com.jt.system.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.system.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("select id,username,password,status " +
"from tb_users " +
"where username=#{username}")
User selectUserByUsername(String username);
/**
* 基于用户id查询用户权限
* @param userId 用户id
* @return 用户的权限
* 涉及到的表:tb_user_roles,tb_role_menus,tb_menus
*/
@Select("select distinct m.permission " +
"from tb_user_roles ur join tb_role_menus rm on ur.role_id=rm.role_id" +
" join tb_menus m on rm.menu_id=m.id " +
"where ur.user_id=#{userId}")
List<String> selectUserPermissions(Long userId);
}
5.3 Service层
package com.jt.system.service.impl;
import com.jt.system.dao.UserMapper;
import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User selectUserByUsername(String username) {
return userMapper.selectUserByUsername(username);
}
@Override
public List<String> selectUserPermissions(long userId) {
//方案一:在这里可以调用数据层的单表查询方法,查询三次获取用户信息
//方案二:调用数据层的多表嵌套或多表关联关联方法执行1次查询
return userMapper.selectUserPermissions(userId);
}
}
package com.jt.system.service;
import com.jt.system.pojo.User;
import java.util.List;
public interface UserService {
User selectUserByUsername(String username);
List<String> selectUserPermissions(long userId);
}
5.4 Controller层
package com.jt.system.controller;
import com.jt.system.pojo.User;
import com.jt.system.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/selectUserByUsername/{username}")
public User selectUserByUsername(@PathVariable("username") String username){
return userService.selectUserByUsername(username);
}
@GetMapping("/selectUserPermissions/{userId}")
public List<String> selectUserPermissions(@PathVariable("userId") long userId){
return userService.selectUserPermissions(userId);
}
}
实现效果
5.5 配置中心配置信息
6. 统一认证工程设计及实现
6.1 创建sso-auth子工程
6.2 配置pom.xml文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--SSO技术方案:SpringSecurity+JWT+oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
6.3 配置bootstrap.yml文件
server:
port: 8071
spring:
application:
name: sso-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
6.4 创建启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class,args);
}
}
实现效果
为spring security内置的登陆页面
访问http://localhost:8071为什么跳到了登陆页面?
因为需要认证
6.5 定义pojo类
package com.jt.auth.pojo;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Long id;
private String username;
private String password;
private String status;
}
6.6 基于feign调用sso-system服务
package com.jt.auth.service;
import com.jt.auth.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(value = "sso-system" )
public interface RemoteUserService {
@GetMapping("/user/login/{username}")
User selectUserByUsername( @PathVariable("username") String username);
@GetMapping("/user/permission/{userId}")
List<String> selectUserPermissions(@PathVariable("userId") Long userId);
}
6.7 定义用户登陆业务逻辑处理对象
package com.jt.auth.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/*在此对象中实现远端服务的调用,从sso-system中获取用户信息,
并对用户信息进行封装并返回给认证管理器,去完成密码的比对操作*/
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private RemoteUserService remoteUserService;
/**
* 基于用户名获取数据库中的用户信息
* @param username 这个username来自客户端
* @return
* @throws UsernameNotFoundException
*/
@Override/*我们执行登录操作的时候提交登录按钮,系统会调用此方法
username来自客户提交的用户名
封装了登录用户信息以及用户权限信息的一个对象
返回UserDetails交给认证管理器*/
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
//基于feign方式获取远程数据并封装
//1.基于用户名获取用户信息
com.jt.auth.pojo.User user=
remoteUserService.selectUserByUsername(username);
if(user==null)
throw new UsernameNotFoundException("用户不存在");
//2.基于用于id查询用户权限
List<String> permissions=
remoteUserService.selectUserPermissions(user.getId());
log.info("permissions {}",permissions);
//3.对查询结果进行封装并返回
User userInfo= new User(username,
user.getPassword(),
AuthorityUtils.createAuthorityList(permissions.toArray(new String[]{})));
return userInfo;
//返回给认证中心,认证中心会基于用户输入的密码以及数据库的密码做一个比对
}
}
6.8 创建SecurityConfig配置类
package com.jt.auth.config;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 当我们在执行登录操作时,底层逻辑(了解):
* 1)Filter(过滤器)
* 2)AuthenticationManager (认证管理器)
* 3)AuthenticationProvider(认证服务处理器)
* 4)UserDetailsService(负责用户信息的获取及封装)
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 初始化加密对象
* 此对象提供了一种不可逆的加密方式,相对于md5方式会更加安全
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 定义认证管理器对象,这个对象负责完成用户信息的认证,
* 即判定用户身份信息的合法性,在基于oauth2协议完成认
* 证时,需要此对象,所以这里讲此对象拿出来交给spring管理
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManagerBean()
throws Exception {
return super.authenticationManager();
}
/**配置认证规则*/
@Override
protected void configure(HttpSecurity http)
throws Exception {
//super.configure(http);//默认所有请求都要认证
//1.禁用跨域攻击(先这么写,不写会报403异常) 这里的登录是post请求 但系统底层的跨域设计是不允许post请求的
http.csrf().disable();
//2.放行所有资源的访问(后续可以基于选择对资源进行认证和放行)
http.authorizeRequests()
.anyRequest().permitAll();
//3.自定义定义登录成功和失败以后的处理逻辑(可选)
//假如没有如下设置登录成功会显示404
http.formLogin()//这句话会对外暴露一个登录路径/login
.successHandler(successHandler())
.failureHandler(failureHandler());
}
//定义认证成功处理器
//登录成功以后返回json数据
@Bean
public AuthenticationSuccessHandler successHandler(){
//lambda
return (request,response,authentication)->{
//构建map对象封装到要响应到客户端的数据
Map<String,Object> map=new HashMap<>();
map.put("state",200);
map.put("message", "login ok");
//将map对象转换为json格式字符串并写到客户端
writeJsonToClient(response,map);
};
}
//定义登录失败处理器
@Bean
public AuthenticationFailureHandler failureHandler(){
return (request,response,exception)->{
//构建map对象封装到要响应到客户端的数据
Map<String,Object> map=new HashMap<>();
map.put("state",500);
map.put("message", "login error");
//将map对象转换为json格式字符串并写到客户端
writeJsonToClient(response,map);
};
}
private void writeJsonToClient(
HttpServletResponse response,
Map<String,Object> map) throws IOException {
//将map对象,转换为json
String json=new ObjectMapper().writeValueAsString(map);
//设置响应数据的编码方式
response.setCharacterEncoding("utf-8");
//设置响应数据的类型
response.setContentType("application/json;charset=utf-8");
//将数据响应到客户端
PrintWriter out=response.getWriter();
out.println(json);
out.flush();
}
}
实现效果
6.9 构建令牌生成及配置对象
借助JWT(Json Web Token-是一种json格式)方式将用户相关信息进行组织和加密,并作为响应令牌(Token)
package com.jt.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/*构建令牌配置对象,在微服务架构中登陆成功可以将用户信息进行存储,常用存储方式?
* 1.产生随机的字符串(token),基于此字符串将用户信息存储到关系数据库(例如mysql)
* 2.产生随机的字符串(token),基于此字符串将用户信息存储到内存数据库(例如redis) 快
* 3.基于JWT创建令牌(token),在此令牌中存储用户信息,此信息不用写到数据库,在客户端存储即可
* 基于如上存储方案,oauth2协议给出了具体的API实现对象,例如
* 1.JdbcTokenStore 用的很少
* 2.RedisTokenStore 中型应用
* 3.JwtTokenStore 对性能要求比较高的分布式架构
* */
@Configuration
public class TokenConfig {
@Bean/*定义令牌存储方案,本次选择基于Jwt令牌方式存储用户状态*/
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean/*配置jwt令牌创建和解析对象*/
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter=new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
//这里的签名key将来可以写到配置中心
private static final String SIGNING_KEY="auth";
}
6.10 定义Oauth2认证授权配置
- 配置认证规则→配置令牌→对谁颁发令牌→定义颁发令牌的路径,解析令牌的路径,校验令牌的路径
package com.jt.auth.config;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.Arrays;
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor/*生成构造函数 为成员变量赋值*/
/*令牌存储和生成的应用方*/
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {//适配器模式0
private AuthenticationManager authenticationManager;
private UserDetailsService userDetailsService;
private TokenStore tokenStore;
private PasswordEncoder passwordEncoder;
private JwtAccessTokenConverter jwtAccessTokenConverter;
/*生成构造函数为成员变量赋值 但是加了@AllArgsConstructor即可省略*/
// public Oauth2Config(AuthenticationManager authenticationManager,
// UserDetailsService userDetailsService,
// TokenStore tokenStore,
// PasswordEncoder passwordEncoder,
// JwtAccessTokenConverter jwtAccessTokenConverter) {
// this.authenticationManager = authenticationManager;
// this.userDetailsService = userDetailsService;
// this.tokenStore = tokenStore;
// this.passwordEncoder = passwordEncoder;
// this.jwtAccessTokenConverter = jwtAccessTokenConverter;
// }
@Override/*配置认证规则*/
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// super.configure(endpoints);
//配置由谁完成认证 认证管理器
endpoints.authenticationManager(authenticationManager)
//配置由谁负责查询用户业务数据
.userDetailsService(userDetailsService)
//处理认证请求的方式 默认只能处理post请求
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
//配置token生成及存储策略 默认为uuid
.tokenServices(tokenServices());
}
@Bean
public AuthorizationServerTokenServices tokenServices(){
//1.创建授权服务对象(TokenServices)
DefaultTokenServices tokenServices=new DefaultTokenServices();
//2.配置令牌的创建和存储对象(TokenStore)
tokenServices.setTokenStore(tokenStore);
//3.配置令牌增强(默认令牌的生成非常简单,使用的是uuid)
TokenEnhancerChain tokenEnhancer=new TokenEnhancerChain();
tokenEnhancer.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
tokenServices.setTokenEnhancer(tokenEnhancer);
//4.设置令牌有效时间
tokenServices.setAccessTokenValiditySeconds(3600);//1h
//5.设置是否支持令牌刷新(是否支持使用刷新令牌再生成新令牌)
tokenServices.setSupportRefreshToken(true);
//6.设置刷新令牌有效时常
tokenServices.setRefreshTokenValiditySeconds(3600*5);
return tokenServices;
}
//1.对谁颁发令牌?(对客户端的要求,如何配置)
//2.访问哪个路径颁发令牌,需要对外暴露认证路径(定义颁发令牌的路径,解析令牌的路径,校验令牌的路径)
/**
* 假如我们要做认证,我们输入了用户名和密码,然后点提交
* ,提交到哪里(url-去哪认证),这个路径是否需要认证?还有令牌过期了,
* 我们要重新生成一个令牌,哪个路径可以帮我们重新生成?
* 如下这个方法就可以提供这个配置 配置对外暴露的url(认证、刷新令牌、检查令牌)
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security)
throws Exception {
security
//1.定义(公开)要认证的url(permitAll()是官方定义好的)
//公开oauth/token_key端点
.tokenKeyAccess("permitAll()") //return this
//2.定义(公开)令牌检查的url
//公开oauth/check_token端点
.checkTokenAccess("permitAll()")
//3.允许客户端直接通过表单方式提交认证
.allowFormAuthenticationForClients();
}
/**
* 认证中心是否要给所有的客户端发令牌呢?假如不是,那要给哪些客户端 客户端有何特点
* 发令牌,是否在服务端有一些规则的定义呢?
* 例如:老赖不能做飞机,不能做高铁
* @param clients 定义客户端的配置详情
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//super.configure(clients);
clients.inMemory()
//定义客户端的id(客户端提交用户信息进行认证时需要这个id)
.withClient("gateway-client")
//定义客户端密钥(客户端提交用户信息时需要携带这个密钥)
.secret(passwordEncoder.encode("123456"))
//定义作用范围(所有符合规则的客户端)
.scopes("all")
//定义允许的任务方式 可以基于密码进行认证 也可以基于刷新令牌进行认证
.authorizedGrantTypes("password", "refresh_token");
}
}
测试效果
- 登陆访问测试
- 检查token信息
- 刷新令牌应用测试
bug集
7.资源服务工程设计及实现
7.1 创建工程sso-resource
7.2 添加依赖
<dependencies>
<!--spring boot web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--在资源服务器添加此依赖,只做授权,不做认证,添加完此依赖以后,
在项目中我们要做哪些事情?对受限访问的资源可以先判断是否登录了,
已经认证用户还要判断是否有权限?
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
7.3 配置bootstrap.yml文件
server:
port: 8881
spring:
application:
name: sso-resource
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
7.4 创建启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ResourceApplication {
public static void main(String[] args) {
SpringApplication.run(ResourceApplication.class,args);
}
}
7.5 创建Controller层
package com.jt.resource.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ResourceController {
@PreAuthorize("hasAnyAuthority('sys:res:create')")//设置访问权限 权限中包含这个字符串才可访问这个资源
@RequestMapping("/resource/select")//ResourceConfig中设置resource开头的请求需要认证
public String doSelect(){
return "Select Resource OK...";
}
/*ResourceConfig中设置res开头的请求直接放行*/
@RequestMapping("/res/delete")
public String doDelete(){
return "Delete resource ok";
}
}
7.6 配置令牌解析器对象
package com.jt.resource.config;
import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
/* 在此配置类中配置令牌的生成,存储策略,验签方式(令牌合法性)。*/
@Configuration
public class TokenConfig {
/*配置令牌的存储策略,对于oauth2规范中提供了这样的几种策略
* 1)JdbcTokenStore(这里是要将token存储到关系型数据库)
* 2)RedisTokenStore(这是要将token存储到redis数据库-key/value)
* 3)JwtTokenStore(这里是将产生的token信息存储客户端,并且token
* 中可以以自包含的形式存储一些用户信息)
* */
@Bean
public TokenStore tokenStore(){
//采用JWT方式生成和存储令牌信息
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter=new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
private static final String SIGNING_KEY="auth";
}
7.7 配置资源认证授权规则
package com.jt.resource.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/*思考?对于一个系统而言,它资源的访问权限你是如何进行分类设计的
* 1)不需要登录就可以访问(例如12306查票)
* 2)登录以后才能访问(例如12306的购票)
* 3)登录以后没有权限也不能访问(例如会员等级不够不让执行一些相关操作)
* */
@Configuration
@EnableResourceServer//启动资源服务默认配置
//访问资源服务器中的相关方法时启动权限调查 与@PreAuthorize配合使用
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {//适配器模式
@Override
public void configure(HttpSecurity http) throws Exception {
// super.configure(http);
//关闭跨域攻. 击
http.csrf().disable();
//配置资源访问方式
http.authorizeRequests().antMatchers("/resource/**")//只要访问这个资源就需要认证
.authenticated()
.anyRequest().permitAll();
}
}
测试
首先需要认证登录,其次需要权限
- 401 需要认证
- 403 无权限
- 404 路径不正确
- 405 请求方式不匹配
8. 网关工程设计及实现
8.1 创建工程sso-gateway
8.2 添加依赖
"> <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--假如网关层面进行限流,添加如下依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
</dependencies>
8.3 配置yml文件
server:
port: 9000
spring:
application:
name: sso-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
config:
server-addr: localhost:8848
file-extension: yml
sentinel:
transport:
dashboard: localhost:8180
eager: true
gateway:
routes:
- id: router01
uri: lb://sso-resource
predicates:
- Path=/sso/resource/**
filters:
- StripPrefix=1
- id: router02
uri: lb://sso-auth
predicates:
- Path=/sso/oauth/**
filters:
- StripPrefix=1
globalcors: #跨域配置(写到配置文件的好处是可以将其配置写到配置中心)
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
allowCredentials: true
8.4 创建启动类
package com.jt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
9. 客户端UI工程设计及实现
9.1 创建sso-ui工程
9.2 创建UI工程登陆页面
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<title>login</title>
</head>
<body>
<div class="container"id="app">
<h3>Please Login</h3>
<form>
<div class="mb-3">
<label for="usernameId" class="form-label">Username</label>
<input type="text" v-model="username" class="form-control" id="usernameId" aria-describedby="emailHelp">
</div>
<div class="mb-3">
<label for="passwordId" class="form-label">Password</label>
<input type="password" v-model="password" class="form-control" id="passwordId">
</div>
<button type="button" @click="doLogin()" class="btn btn-primary">Submit</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
var vm=new Vue({
el:"#app",//定义监控点,vue底层会基于此监控点在内存中构建dom树
data:{ //此对象中定义页面上要操作的数据
username:"",
password:""
},
methods: {//此位置定义所有业务事件处理函数
doLogin() {
//1.定义url
let url = "http://localhost:9000/sso/oauth/token"
//2.定义参数
let params = new URLSearchParams()
params.append('username',this.username);
params.append('password',this.password);
params.append('client_id',"gateway-client");
params.append('client_secret',"123456");
params.append('grant_type',"password");
//3.发送异步请求
axios.post(url, params)
.then((response) => {//ok
alert("login ok")
let result=response.data;
console.log("result",result);
//将返回的访问令牌存储到浏览器本地对象中
localStorage.setItem("accessToken",result.access_token);
location.href="/resource.html";
//启动一个定时器,一个小时以后,向认证中心发送刷新令牌
})
.catch((e)=>{
console.log(e);
})
}
}
});
</script>
</body>
</html>
9.3 创建资源展现页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>The Resource Page</h1>
<button onclick="doSelect()">我的资源(例如我的订单)</button>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
function doSelect(){
let url="http://localhost:9000/sso/resource/select";
//获取登录后,存储到浏览器客户端的访问令牌
let token=localStorage.getItem("accessToken");
//发送请求时,携带访问令牌
axios.get(url,{headers:{"Authorization":"Bearer "+token}})
.then(function (response){
alert("select ok")
console.log(response.data);
})
.catch(function (e){//失败时执行catch代码块
if(e.response.status==401){
alert("请先登录");
location.href="/login.html";
}else if(e.response.status==403){
alert("您没有权限")
}
console.log("error",e);
})
}
</script>
</body>
</html>
实现效果
总结
- 单点登陆系统解决方案?数据库、token
- 为什么要设计单点登录系统??
- 工程结构???
单体架构设计
httpsession(30mins,tomcat私有) cookie(存储sessionid) server