一、授权服务器的定位

一言而概之:就是为客户端产生一个Token
如图所示:
在这里插入图片描述

二、授权服务器的实现

2.1 添加依赖

<!--        服务发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.6.RELEASE</verison>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.6.RELEASE</verison>
        </dependency>

2.2 配置文件

spring:
  application:
    name: authorization-server
  cloud:
    nacos:
      discovery:
        server-addr: nacos-server:8848
server:
  port: 9999

2.3 启动类

package com.zhubayi.authorization;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * @author zhubayi
 */
@SpringBootApplication
@EnableDiscoveryClient
public class AuthorizationApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthorizationApplication.class,args);
    }

}

2.4配置类

2.4.1 授权服务器的配置

@EnableAuthorizationServer
@Configuration
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    public PasswordEncoder passwordEncoder ;

    @Autowired
    private AuthenticationManager authenticationManager ;

    @Autowired
    private UserDetailsService userDetailsService ;
    /**
     * 配置第三方客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("coin-api") //客户端id
                .secret(passwordEncoder.encode("coin-secret")) //客户端密码,要加密,不然一直要求登录,获取不到临牌,一定不要泄露
                .scopes("all")//授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
                .authorizedGrantTypes("password","refresh_token")//refresh_token配置这个才能刷新令牌 grant_type对应 password
                 .autoApprove(false)//false 跳转到授权页面手动点击,true不用手动点击,直接响应授权
                .accessTokenValiditySeconds(24 * 3600) //24小时
                .refreshTokenValiditySeconds(7 *  24 * 3600);//七天
    }

    /**
     * 设置授权管理器和UserDetailsService 内存认证
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new InMemoryTokenStore())
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService) ;
    }
}

2.4.2 Web 安全的配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 注入一个验证管理器
     *
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 资源的放行
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); // 关闭scrf 跨域
        http.authorizeRequests().anyRequest().authenticated();
    }


    /**
     * 创建一个测试的UserDetail
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        User user = new User("admin", "123456", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"))) ;
        inMemoryUserDetailsManager.createUser(user);
        return inMemoryUserDetailsManager;
    }

    /**
     * 注入密码的验证管理器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

2.5 获取token测试

第一步:
在这里插入图片描述
第二步:
在这里插入图片描述

三、JWT接入

3.1 Token共享问题

我的token 目前存储在内存里面:
在这里插入图片描述

也就是说,当我们仅仅只有一台authorization-server 时,没有任何问题,但是当我们使用多台authorization-server时,由于内存数据无法共享,故用户登录的数据仅仅保存在一台服务器里面,这就会导致某台授权服务器会误判“是否用户登录”这个问题。

在这里插入图片描述

3.2 使用Redis 共享Token

将之前数据存储在内存里面的问题解决掉,现在直接把token 存储在内存里面:
在这里插入图片描述

3.2.1 添加依赖

<!--redis-->
 <dependency>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.3.2.RELEASE</version>
 </dependency>

3.2.2 添加配置文件

server:
  port: 9999
spring:
  application:
    name: authorization-server
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

  redis:
    port: 6379
    host: 127.0.0.1
    password: 123456
    database: 0 #指定数据库

注意:redis-server 要事先在host文件里面配置。

3.2.3 使用RedisTokenStore

修改我们之前的AuthorizationServerConfig配置类:

@EnableAuthorizationServer // 开启授权服务器的功能
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder ;

    @Autowired
    private AuthenticationManager authenticationManager ;

    @Autowired
    private UserDetailsService userDetailsService ;
    //注入redis
    @Autowired
    private RedisConnectionFactory redisConnectionFactory ;


    /**
     *  添加第三方的客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("coin-api") // 第三方客户端的名称
                .secret(passwordEncoder.encode("coin-secret")) //  第三方客户端的密钥
                .scopes("all") //第三方客户端的授权范围
                .accessTokenValiditySeconds(24*3600) // token的有效期
                .refreshTokenValiditySeconds(24*7*3600);// refresh_token的有效期
        super.configure(clients);
    }

    /**
     * 配置验证管理器,UserdetailService
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(redisTokenStore());
        super.configure(endpoints);
    }


    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory) ;
    }
}

3.2.4 获取Token 测试

重启authorization-server,获取token测试
在这里插入图片描述

在这里插入图片描述

3.2.5 观察Redis数据

我们发现,在redis 里面已经保存了用户登录的数据了。
在这里插入图片描述

3.3 资源服务器和授权服务的交互

在这里插入图片描述

3.3.1 在授权服务器里面准备userinfo的接口

在这里插入图片描述

@RestController
public class UserInfoController {

    /**
     * 获取该用户的对象
     * @param principal
     * @return
     */
    @GetMapping("/user/info")
    public Principal usrInfo(Principal principal){ // 此处的principal 由OAuth2.0 框架自动注入
        // 原理就是:利用Context概念,将授权用户放在线程里面,利用ThreadLocal来获取当前的用户对象 
//        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return principal ;
    }
}

3.3.2 将该授权服务器变成资源服务器

因为授权服务器里面提供了userinfo 该资源,所以我们也将它认为是授权服务器。
添加配置类就可以了。
在这里插入图片描述

@EnableResourceServer
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}

3.3.2使用token 换取user对象

第一步:获取一个token
在这里插入图片描述

在这里插入图片描述

第二步:使用Token 换用户对象
在这里插入图片描述
我们可以看见,user 对象已经被获取到了。

3.3.3 存在的问题

我们在理解资源服务器和授权服务的交互后会发现,授权服务器有巨大的压力:当用户访问每一个受保护的资源时(无论该资源散落在那个微服务里面),资源服务器都要和授权服务器交互一次,这样,授权服务器的压力将非常的大。我们必须找解决方案。

  • 方案一:使用负载均衡的概念,多部署几台授权服务器
  • 方案二:让资源服务器不再访问授权服务器

3.4 使用Jwt来做token的存储

上面的方案里面,我们提到了让资源服务器不再访问授权服务器,那会存在什么问题呢?
资源服务器访问授权服务的本质在于2点:

  • 第一点:资源服务器无法验证token的正确性,因为它没有存储token
  • 第二点:资源服务要通过授权服务器来换取用户(token 换 user)。

我们来推演:资源服务器当前只能得到用户给他的token我们能做的改造有限

  • 第一步:若我们将用户的基本信息存储在token 里面呢?
  • 第二步:定义一种加密规则,让资源服务器也能去判断该token的正确性。
    这样,我们的JWT就上场了。看看JWT的定义:
    在这里插入图片描述

3.4.1 生成私钥和公钥

生成私钥:

keytool -genkeypair -alias coinexchange -keyalg RSA -keypass coinexchange -keystore coinexchange.jks -validity 365 -storepass coinexchange

在这里插入图片描述
具体命令和参数:
Keytool 是一个java提供的证书管理工具

在这里插入图片描述
现在,刚刚运行命令的目录上已经有一个jks 文件了。
在这里插入图片描述
该文件里面保存的就是私钥信息。
解析公钥:
要去下载openssl
openssl下载地址
我下的这个
在这里插入图片描述
一直点下一步然后配置环系统境变量。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

就是你自己openssl安装的目录
安装之后就可以解析公钥了

keytool -list -rfc --keystore coinexchange.jks | openssl x509 -inform pem -pubkey

在这里插入图片描述
要输入你刚刚创建时输入的密码
将解析出来的公钥放在一个文件的文件里面:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArbzkbzTuolRUWzdGUfj/
cc5BHuQeTXUJuvfHtEFQf5yl2ZZ9Q6banG5Bb9ph9/v5C1BjeoJYtzJoiMfHUOFs
BLIYwseII4pt38OQJ4SVu1okOEPv+mgbNxHdyfX0etROCKKFBQrvV+N21IO/meRJ
YlXylmWt4/wh78G3jgXFsnCr/VAUqRGxDPA+r3zAXNAFXAiJFEOzvBq+8+QLQ/hv
lzN2asfr0M4b/N1mgO6N3atpat3updLD0zzOZ0P8vDhJzNCgPTQe5urxoSg8BH1M
BIH8Qx3Mfwq5Lf+SZjCWKzRZpw047MH3ReEER4E0s1F0mmS5MEMWsjrlzzTzY+T7
ewIDAQAB
-----END PUBLIC KEY-----

在这里插入图片描述

保存好,以后备用。

3.4.2 修改配置文件,不要redis了

server:
  port: 9999
spring:
  application:
    name: authorization-server
  cloud:
    nacos:
      discovery:
        server-addr: nacos-server:8848

pom文件redis的依赖也不要了。
在这里插入图片描述
删了注释了也行

3.4.3 将私钥文件复制到resource下

在这里插入图片描述

3.4.4 修改配置类载入私钥文件

@EnableAuthorizationServer // 开启授权服务器的功能
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder ;

    @Autowired
    private AuthenticationManager authenticationManager ;

    @Autowired
    private UserDetailsService userDetailsService ;

//    @Autowired
//    private RedisConnectionFactory redisConnectionFactory ;


    /**
     *  添加第三方的客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("coin-api") // 第三方客户端的名称
                .secret(passwordEncoder.encode("coin-secret")) //  第三方客户端的密钥
                .scopes("all") //第三方客户端的授权范围
                .accessTokenValiditySeconds(24*3600) // token的有效期
                .refreshTokenValiditySeconds(24*7*3600);// refresh_token的有效期
        super.configure(clients);
    }

    /**
     * 配置验证管理器,UserdetailService
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                // .tokenStore(redisTokenStore()); 不用redis了
                .tokenStore(jwtTokenStore()) //设置token 存储在哪里
                .tokenEnhancer(jwtAccessTokenConverter()) ;
        super.configure(endpoints);
    }

   //不用了redis,用jwt
//    public TokenStore redisTokenStore(){
//        return new RedisTokenStore(redisConnectionFactory) ;
//    }

    /**
     * jwtTokenStore
     * @return
     */
    public  TokenStore jwtTokenStore(){
        JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter());
        return jwtTokenStore ;
    }

    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter() ;
        // 读取classpath 下面的密钥文件
        ClassPathResource classPathResource = new ClassPathResource("coinexchange.jks");
        // 获取KeyStoreFactory
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,"coinexchange".toCharArray()) ;
        // 给JwtAccessTokenConverter 设置一个密钥对
        tokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("coinexchange","coinexchange".toCharArray()));
        return  tokenConverter ;
    }
}

3.4.5 获取token 测试

在这里插入图片描述

在这里插入图片描述

3.4.6 使用jwt.io 校验token

地址:https://jwt.io/
在这里插入图片描述

四、JWT的登出问题

Jwt 使用起来不难,而且让我们将“无状态”的概念更贴切的展示出来了,但是实践就真的这么完美吗?不是,因为jwt的登出问题。
何为登出:就是用户自己点击登出后,或用户的角色/权限改变后,该token 仍然是有效的。你可以选择在前端清除该token,但是,如果用户是有技术背景的黑客呢?之前的token他保存一边,在没有过期(时间过期)时,他仍然可以使用该token。
解决方案:
在这里插入图片描述
在这里插入图片描述

就是删除该用户存储在redis里面登录的token数据

4.1 在网关里面判断该token是否存在

在这里插入图片描述
老师的源码:

@Component
public class JwtCheckFilter implements GlobalFilter, Ordered {

    @Autowired
    private StringRedisTemplate redisTemplate ;
    
    @Value("${no.require.urls:/admin/login,/user/gt/register,/user/login,/user/users/register,/user/sms/sendTo,/user/users/setPassword}")
    private Set<String> noRequireTokenUris ;
    /**
     * 过滤器拦截到用户的请求后做啥
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1 : 该接口是否需要token 才能访问
        if(!isRequireToken(exchange)){
            return chain.filter(exchange) ;// 不需要token ,直接放行 
        }
        // 2: 取出用户的token
        String token = getUserToken(exchange) ;
        // 3 判断用户的token 是否有效
        if(StringUtils.isEmpty(token)){
            return buildeNoAuthorizationResult(exchange) ;
        }
        Boolean hasKey = redisTemplate.hasKey(token);
        if(hasKey!=null && hasKey){
            return chain.filter(exchange) ;// token有效 ,直接放行 
        }
        return buildeNoAuthorizationResult(exchange) ;
    }

    /**
     * 给用户响应一个没有token的错误
     * @param exchange
     * @return
     */
    private Mono<Void> buildeNoAuthorizationResult(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("Content-Type","application/json");
        response.setStatusCode(HttpStatus.UNAUTHORIZED) ;
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("error","NoAuthorization") ;
        jsonObject.put("errorMsg","Token is Null or Error") ;
        DataBuffer wrap = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
        return response.writeWith(Flux.just(wrap)) ;
    }

    /**
     * 从 请求头里面获取用户的token
     * @param exchange
     * @return
     */
    private String getUserToken(ServerWebExchange exchange) {
        String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        return token ==null ? null : token.replace("bearer ","") ;
    }

    /**
     * 判断该 接口是否需要token
     * @param exchange
     * @return
     */
    private boolean isRequireToken(ServerWebExchange exchange) {
        String path = exchange.getRequest().getURI().getPath();
        if(noRequireTokenUris.contains(path)){
            return false ; // 不需要token
        }
        return Boolean.TRUE ;
    }


    /**
     * 拦截器的顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

自己写的:

@Component
public class TokenCheckFilter implements GlobalFilter, Ordered {
    @Value("${no.token.access.urls:/admin/login,/admin/validate/code}")
    private Set<String> noTokenAccessUrls;
    /**
     * 实现判断用户是否携带token ,或token 错误的功能
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //不需要token就能访问
        if(allowNoTokenAccess(exchange)){
            return chain.filter(exchange);
        }
        // 获取用户的token
        String token=getToken(exchange);
        //token为空
        if (StringUtils.isEmpty(token)) { // token 为 Empty
            return buildUNAuthorizedResult(exchange);
        }
        return chain.filter(exchange);



    }

    private Mono<Void> buildUNAuthorizedResult(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);//未认证
        response.getHeaders().set("Content-Type","application/json;charset=UTF-8");
        HashMap<String, String> map = new HashMap<>();
        map.put("error","unauthorized");
        map.put("error_description", "invalid_token");
        DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
        return response.writeWith(Flux.just(dataBuffer));
    }

    /**
     * 获取用户token
     * @param exchange
     * @return
     */
    private String getToken(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION);
        if(Objects.isNull(authorization)||authorization.trim().isEmpty()){
            return null;
        }
        return authorization.replace("bearer","");
    }
    

    private boolean allowNoTokenAccess(ServerWebExchange exchange){
        String path = exchange.getRequest().getURI().getPath();
        if(noTokenAccessUrls.contains(path)){
            //放行登录、验证接口
            return true;
        }
        return false;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

4.2 模拟访问

访问 /admin/login 这样的不需要token的资源时:
在这里插入图片描述
访问启动的资源时:
在这里插入图片描述

五、授权服务器的接入

之前我们仅仅在authorizaiton-server 里面添加了一个模拟的用户:
在这里插入图片描述
还没有接入到我们的系统的用户数据,本节课我们来接入一下我们的用户数据

5.1 添加登录常量

在该登录常量里面,我们可以定义受支持的登录类型

public class LoginConstant {

    /**
     * 管理员登录
     */
    public static final String ADMIN_TYPE = "admin_type" ;

    /**
     * 用户/会员登录
     */
    public static final String MEMBER_TYPE  = "member_type" ;


}

5.2 实现UserDetailService 接口

在这里插入图片描述
在这里插入图片描述

/**
 * 登录的实现
 *
 * @param username
 * @return
 * @throws UsernameNotFoundException
 */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    String loginType = requestAttributes.getRequest().getParameter("login_type");
    if (StringUtils.isEmpty(loginType)) {
        throw new AuthenticationServiceException("请添加login_type参数");
    }
    UserDetails userDetails = null;
    switch (loginType) {
        case LoginConstant.ADMIN_TYPE: // 管理员登录
            userDetails = loadAdminUserByUsername(username);
            break;
        case LoginConstant.MEMBER_TYPE: // 会员登录
            userDetails = loadMemberUserByUsername(username);
            break;
        default:
            throw new AuthenticationServiceException("暂不支持的登录方式" + loginType);
    }
    return userDetails;
}

5.3 管理员用户的登录

5.3.1 RBAC模型

RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
在这里插入图片描述

sys_user表

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '账号',
  `password` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
  `fullname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
  `mobile` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态 0-无效; 1-有效;',
  `create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',
  `modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',
  `created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1018715142409592835 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '平台用户' ROW_FORMAT = Dynamic;

角色表sys_role

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',
  `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '代码',
  `description` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
  `create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',
  `modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',
  `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态0:禁用 1:启用',
  `created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1017767747970568195 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色' ROW_FORMAT = Dynamic;

用户角色关联表sys_user_role

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role_id` bigint(18) NULL DEFAULT NULL COMMENT '角色ID',
  `user_id` bigint(18) NULL DEFAULT NULL COMMENT '用户ID',
  `create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',
  `modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',
  `created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1022060671264763907 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色配置' ROW_FORMAT = Dynamic;

权限表sys_privilege

-- ----------------------------
-- Table structure for sys_privilege
-- ----------------------------
DROP TABLE IF EXISTS `sys_privilege`;
CREATE TABLE `sys_privilege`  (
  `id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `menu_id` bigint(18) NULL DEFAULT NULL COMMENT '所属菜单Id',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '功能点名称',
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '功能描述',
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `create_by` bigint(18) NULL DEFAULT NULL COMMENT '创建人',
  `modify_by` bigint(18) NULL DEFAULT NULL COMMENT '修改人',
  `created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `unq_name`(`name`(191)) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1010101010101010193 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '权限配置' ROW_FORMAT = Dynamic;

角色权限关联表sys_role_privilege

-- ----------------------------
-- Table structure for sys_role_privilege
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_privilege`;
CREATE TABLE `sys_role_privilege`  (
  `id` bigint(18) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(18) NOT NULL,
  `privilege_id` bigint(18) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1021574920613801987 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色权限配置' ROW_FORMAT = Dynamic;

user表

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint(18) NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `type` tinyint(4) NULL DEFAULT 1 COMMENT '用户类型:1-普通用户;2-代理人',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
  `country_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '国际电话区号',
  `mobile` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
  `paypassword` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '交易密码',
  `paypass_setting` tinyint(1) NULL DEFAULT 0 COMMENT '交易密码设置状态',
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `real_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '真实姓名',
  `id_card_type` tinyint(1) NULL DEFAULT NULL COMMENT '证件类型:1,身份证;2,军官证;3,护照;4,台湾居民通行证;5,港澳居民通行证;9,其他;',
  `auth_status` tinyint(4) NULL DEFAULT 0 COMMENT '认证状态:0-未认证;1-初级实名认证;2-高级实名认证',
  `ga_secret` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Google令牌秘钥',
  `ga_status` tinyint(1) NULL DEFAULT 0 COMMENT 'Google认证开启状态,0,未启用,1启用',
  `id_card` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '身份证号',
  `level` int(11) NULL DEFAULT NULL COMMENT '代理商级别',
  `authtime` datetime(0) NULL DEFAULT NULL COMMENT '认证时间',
  `logins` int(11) NULL DEFAULT 0 COMMENT '登录数',
  `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态:0,禁用;1,启用;',
  `invite_code` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '邀请码',
  `invite_relation` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '邀请关系',
  `direct_inviteid` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '直接邀请人ID',
  `is_deductible` int(11) NULL DEFAULT 0 COMMENT '0 否 1是  是否开启平台币抵扣手续费',
  `reviews_status` int(11) NULL DEFAULT 0 COMMENT '审核状态,1通过,2拒绝,0,待审核',
  `agent_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '代理商拒绝原因',
  `access_key_id` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'API的KEY',
  `access_key_secret` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'API的密钥',
  `refe_auth_id` bigint(30) NULL DEFAULT NULL COMMENT '引用认证状态id',
  `last_update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
  `created` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `status`(`status`) USING BTREE,
  INDEX `idx_addtime`(`created`) USING BTREE,
  INDEX `username`(`username`(191)) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1024859055654637571 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC;

5.3.2 依赖导入

我们选择简单的jdbcTemplate 来做权限的查询操作

<!--连接数据库查用户信息和权限-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>
        <!--数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

5.3.3 配置数据源

server:
  port: 9999
spring:
  application:
    name: authorization-server
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/coin-exchange?useSSL=false&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456

5.3.4 准备SQL语句

以下sql 语句都位于LoginConstant 常量里面

一、用于登录

使用用户名查询用户的SQL:

public static final String QUERY_ADMIN_SQL =
        "SELECT `id` ,`username`, `password`, `status` FROM sys_user WHERE username = ? ";
二、查询用户的权限
  1. 判断用户是否为管理员
public static final String QUERY_ROLE_CODE_SQL =
        "SELECT `code` FROM sys_role LEFT JOIN sys_user_role ON sys_role.id = sys_user_role.role_id WHERE sys_user_role.user_id= ?";
  1. 用户为管理员时:(拥有全部的权限)
public static final String QUERY_ALL_PERMISSIONS =
        "SELECT `name` FROM sys_privilege";
  1. 普通用户时(通过用户的角色查询用户的权限)
public static final String QUERY_PERMISSION_SQL =
        "SELECT * FROM sys_privilege LEFT JOIN sys_role_privilege ON sys_role_privilege.privilege_id = sys_privilege.id LEFT JOIN sys_user_role  ON sys_role_privilege.role_id = sys_user_role.role_id WHERE sys_user_role.user_id = ?";

5.3.5 代码实现

/**
 * 登录的实现
 *
 * @param username
 * @return
 * @throws UsernameNotFoundException
 */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    String loginType = requestAttributes.getRequest().getParameter("login_type");
    if (StringUtils.isEmpty(loginType)) {
        throw new AuthenticationServiceException("请添加login_type参数");
    }
    UserDetails userDetails = null;
    try {
        switch (loginType) {
            case LoginConstant.ADMIN_TYPE: // 管理员登录
                userDetails = loadAdminUserByUsername(username);
                break;
            case LoginConstant.MEMBER_TYPE: // 会员登录
                userDetails = loadMemberUserByUsername(username);
                break;
            default:
                throw new AuthenticationServiceException("暂不支持的登录方式" + loginType);
        }
    } catch (IncorrectResultSizeDataAccessException e) {
        throw new UsernameNotFoundException("会员:" + username + "不存在");
    }
    return userDetails;
}


/**
 * 对接管理员的登录
 *
 * @param username
 * @return
 */
private UserDetails loadAdminUserByUsername(String username) {
    return jdbcTemplate.queryForObject(QUERY_ADMIN_SQL, new RowMapper<User>() {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            if (rs.wasNull()) {
                throw new UsernameNotFoundException("用户:" + username + "不存在");
            }
            Long id = rs.getLong("id");
            String password = rs.getString("password");
            int status = rs.getInt("status");
            User user = new User(
                    String.valueOf(id), // 使用用户的id 代替用户的名称,这样会使得后面的很多情况得以处理
                    password,
                    status == 1,
                    true,
                    true,
                    true,
                    getUserPermissions(id));
            return user;
        }
    }, username);
}

/**
 * 通过用户的id 获取用户的权限
 *
 * @param id
 * @return
 */
private Set<SimpleGrantedAuthority> getUserPermissions(Long id) {
    // 查询用户是否为管理员
    String code = jdbcTemplate.queryForObject(QUERY_ROLE_CODE_SQL, String.class, id);
    List<String> permissions = null;
    if (ADMIN_CODE.equals(code)) { // 管理员
        permissions = jdbcTemplate.queryForList(QUERY_ALL_PERMISSIONS, String.class);
    } else {
        permissions = jdbcTemplate.queryForList(QUERY_PERMISSION_SQL, String.class, id);
    }
    if (permissions == null || permissions.isEmpty()) {
        return Collections.EMPTY_SET;
    }
    return permissions
            .stream()
            .distinct() // 去重
            .map(
                    perm -> new SimpleGrantedAuthority(perm) // perm - >security可以识别的权限
            )
            .collect(Collectors.toSet());
}


5.3.6 测试效果

注意,先将数据库里面sys_user表里面,admin用户的密码修改为admin
在这里插入图片描述
这样测试起来简单一点。
在这里插入图片描述
我们发现,已经获取到了Token,看看token 里面都藏了什么:
在这里插入图片描述
登录已经完成了

5.3.7 密码加密器

在这里插入图片描述
修改:WebSecurityConfig:将之前的PasswordEncoder 修改为以下的代码:

	/**
     * 注入密码的验证管理器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new   BCryptPasswordEncoder();
    }

我们的密码加密器将会影响2 个地方:

  1. 第三方客户端

在这里插入图片描述
2. 用户登录时的密码匹配
在这里插入图片描述

5.3.8 测试密码匹配器的效果

先获取测试数据:
在这里插入图片描述
在这里插入图片描述

public static void main(String[] args) {
        System.out.println( new BCryptPasswordEncoder().encode("123456"));
    }

先将数据库里面sys_user表里面,admin用户的密码修改为刚刚输出的:
$2a$10$sYSgrL0P4l/1UTlK/slXkuypXRGc.D.iV7uzhE3545a8yKdEw8XaG
在这里插入图片描述

在这里插入图片描述
获取Token:
在这里插入图片描述
还是没有任何的错误。

5.4 会员登录的接入

会员没有复杂的RBAC模型处理,我们仅仅做简单的登录就可以了。

5.4.1 准备SQL 语句

public static final String QUERY_MEMBER_SQL =
        "SELECT `id`,`password`, `status` FROM `user` WHERE mobile = ? or email = ? ";

5.4.2 代码实现

/**
     * 会员登录
     * @param name
     * @return
     */
    private UserDetails loadMemberUserByUsername(String name) {
        return jdbcTemplate.queryForObject(LoginConstant.QUERY_MEMBER_SQL,(rs, rowNum)->{
            if(rs.wasNull()){
                throw new UsernameNotFoundException("会员:" + name + "不存在");
            }
            long id = rs.getLong("id"); // 获取用户的id
            String password = rs.getString("password");
            int status = rs.getInt("status");
            return new User(
                    String.valueOf(id),
                    password,
                    status == 1 ,
                    true ,
                    true ,
                    true,
                    Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))
            );
    },name,name);
    }

5.4.3 测试登录

在这里插入图片描述

六、refresh_token和过期时间

我们可以可以使用refresh_token 来为过期的token 获取一个新的token数据

6.1 添加验证方式

在这里插入图片描述

6.2 获取信息的token 测试

重启后重新后期:
在这里插入图片描述
大家可以看见,我们获取到的数据新增了refresh_token的一项,我们来看看它里面包含那些信息:
在这里插入图片描述
基本和之前时没有区别的。

6.3 使用Refresh_token获取新的token

先看错误:
在这里插入图片描述
原因在于:我们把jwt 里面的username 换成了现在的 用户 id ,导致的。
现在,我们需要一个纠正的过程:
Refresh_token的标识:(loginConstant)

/**
 * token的刷新
 */
public static  final  String REFRESH_TOKEN = "REFRESH_TOKEN" ;

@Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        String loginType = requestAttributes.getRequest().getParameter("login_type");
        if (StringUtils.isEmpty(loginType)){
            throw new AuthenticationServiceException("请添加login_type参数");
        }
        //刷新令牌
        String grantType = requestAttributes.getRequest().getParameter("grant_type");
        UserDetails userDetails=null;
        try {
            if (LoginConstant.REFRESH_TOKEN.equals(grantType.toUpperCase())) {
                name = adjustUsername(name, loginType); // 为refresh_token 时,需要将id->username
            }
            switch (loginType){
                case LoginConstant
                        .ADMIN_TYPE: //管理员登录
                    userDetails=loadAdminUserByUsername(name);
                    break;
                case LoginConstant.MEMBER_TYPE://会员登录
                    userDetails = loadMemberUserByUsername(name);
                    break;
                default:
                    throw new AuthenticationServiceException("暂不支持的登录方式" + loginType);
            }
        } catch (AuthenticationServiceException e) {
            throw new UsernameNotFoundException("会员:" + name + "不存在");
        }
        return userDetails;
    }

纠正的实现:
添加SQL语句:

/**
 * 使用用户的id 查询用户名称
 */
public static  final  String QUERY_ADMIN_USER_WITH_ID = "SELECT `username` FROM sys_user where id = ?" ;

/**
 * 使用用户的id 查询用户名称
 */
public static  final  String QUERY_MEMBER_USER_WITH_ID = "SELECT `mobile` FROM user where id = ?" ;

实现纠正:

  /**
     * 纠正在refresh 场景下的登录问题
     * @param username
     * @param loginType
     * @return
     */
    private String adjustUsername(String username, String loginType) {
        if(LoginConstant.ADMIN_TYPE.equals(loginType)){
            //管理员
            return jdbcTemplate.queryForObject(LoginConstant.QUERY_ADMIN_USER_WITH_ID,String.class,username);
        }
        if(LoginConstant.MEMBER_TYPE.equals(loginType)){
            return jdbcTemplate.queryForObject(LoginConstant.QUERY_MEMBER_USER_WITH_ID,String.class,username);
        }
        return username;
    }

再测试一下:
参数:

grant_type :refresh_token
login_type:admin_type
refresh_token:刚刚测试得到的refresh_token

在这里插入图片描述
在这里插入图片描述

6.4 token 过期时间的设置

在这里插入图片描述
Token的有效期为一周,
Refresh_token的有效期为一个月。

七、Token传递和获取

7.1 受保护资源之前Token的传递

Case1:
在这里插入图片描述
Case2:
在这里插入图片描述
在第一种Case 里面,我们可以从本次请求的上下文里面获取用户的token ,进行一个Token的传递。
在第二种Case 里面,我们没有一个用户请求的上下文,因此我们需要应用自己去获取一个临时的token。
这2种请求的源码实现,OAuth2.0 已经帮我们写好了,在:
在这里插入图片描述
获取的方式非常的简单:使用client_credentials 授权方式来进行的。

7.2 在authorization-server 里面添加客户端授权的方式

 /**
     * 配置第三方客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("coin-api")//客户端id
                .secret(passwordEncoder.encode("coin-secret"))//客户端密码,要加密,不然一直要求登录,获取不到临牌,一定不要泄露
                .scopes("all")//授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
                .authorizedGrantTypes("password","refresh_token") //refresh_token配置这个才能刷新令牌 grant_type对应 password
                .autoApprove(false)//false 跳转到授权页面手动点击,true不用手动点击,直接响应授权
                //.redirectUris()//客户端回调地址
                .accessTokenValiditySeconds(1*60*60)//一个小时
                .refreshTokenValiditySeconds(60*60*2)//临牌刷新实际
                .and()
                .withClient("inside-app")
                .secret(passwordEncoder.encode("inside-secret"))
                .scopes("all")
                .authorizedGrantTypes("client_credentials")
                .accessTokenValiditySeconds(7 * 24 *3600) ;
		        super.configure(clients);
    }

在这里插入图片描述

7.3 获取token 测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

成功拿到了token!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐