2.若依中的认证中心

1.前言

   在本节中主要是介绍若依微服务版本中的认证功能以及实现流程,认证功能主要包含注册、登录认证,用户注销,刷新token等。

1.1 什么是认证中心

  身份认证,就是判断一个用户登录是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。

1.2 为什么要使用认证中心

  登录请求后台接口,为了安全认证,所有请求都携带token信息进行安全认证,来比较登录用户的身份与系统存储的信息是否一致。

 2.使用认证

2.1 原理介绍

2.1.1 登录认证

  登录认证即系统登录用户的认证过程。TokenController控制器login方法会进行用户验证,如果验证通过会保存登录日志并返回token,同时缓存中会存入login_tokens:xxxxxx(包含用户、权限信息)。

2.1.2 刷新令牌

  刷新令牌就是对系统操作用户进行缓存刷新,防止过期。TokenController控制器refresh方法会在用户调用时更新令牌有效期。

  

2.1.3 用户注销

  用户注销是系统登录用户的退出过程。TokenController控制器logout方法会在用户退出时删除缓存信息同时保存用户退出日志。

 2.2 代码实现

2.2.1 引入依赖

  在该依赖中包含nacos注册发现、配置、sentinel、web、Actuator、ruoyi-common-security等依赖包,其中ruoyi-common-security需要我们导入依赖并搭建项目作为ruoyi-auth模块的子模块

<dependencies>
        
        <!-- SpringCloud Alibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        
        <!-- SpringCloud Alibaba Nacos Config -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        
        <!-- SpringCloud Alibaba Sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        
        <!-- SpringBoot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- SpringBoot Actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- RuoYi Common Security-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common-security</artifactId>
        </dependency>
        
    </dependencies>
ruoyi-auth依赖

  在ruoyi-common-security模块中包含webmvc、ruoyi-system、ruoyi-common-redis等依赖

<dependencies>

        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
        </dependency>

        <!-- RuoYi Api System -->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-api-system</artifactId>
        </dependency>

        <!-- RuoYi Common Redis-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common-redis</artifactId>
        </dependency>

    </dependencies>
ruoyi-common-security依赖

  在ruoyi-common-redis模块中包含redis、ruoyi-common-core等依赖

<dependencies>
        
        <!-- SpringBoot Boot Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- RuoYi Common Core-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common-core</artifactId>
        </dependency>
        
    </dependencies>
ruoyi-common-redis依赖

  在ruoyi-common-core模块中包含OpenFeign、loadBalancer、SpringContextSupport、web、Transmittable ThreadLocal、PageHelper、Hibernate Validator、Jackson、FastJSON、Jwt、jaxb、lang、IO、excel、servlet、swagger等依赖。

<dependencies>

        <!-- SpringCloud Openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        
        <!-- SpringCloud Loadbalancer -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <!-- Spring Context Support -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>

        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>

        <!-- Transmittable ThreadLocal -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
        </dependency>

        <!-- Pagehelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>

        <!-- Hibernate Validator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- Jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <!-- Alibaba Fastjson -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>

        <!-- Jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>

        <!-- Jaxb -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>

        <!-- Apache Lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- Commons Io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>

        <!-- excel工具 -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
        </dependency>

        <!-- Java Servlet -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>

        <!-- Swagger -->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
        </dependency>

    </dependencies>
ruoyi-common-core

2.2.2 编写bootstrap.yml文件

   在该配置文件中包该模块的服务端口、应用名称、环境、nacos注册地址、配置中心地址、配置格式以及共享配置文件。

# Tomcat
server: 
  port: 9200

# Spring
spring: 
  application:
    # 应用名称
    name: ruoyi-auth
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址
        server-addr: 127.0.0.1:8848
        # 配置文件格式
        file-extension: yml
        # 共享配置,即将application.dev.yml作为共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
bootstrap.yml文件

2.2.3 form类(注册和登录实体类)

  在用户登录和注册实体类中,注册实体类继承了登录实体类,他们都只有username和password字段以及对应的get和set方法

package com.ruoyi.auth.form;

/**
 * 用户登录对象
 * 
 * @author ruoyi
 */
public class LoginBody
{
    /**
     * 用户名
     */
    private String username;

    /**
     * 用户密码
     */
    private String password;

    public String getUsername()
    {
        return username;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }

    public String getPassword()
    {
        return password;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }
}
用户登录实体类
package com.ruoyi.auth.form;

/**
 * 用户注册对象
 * 
 * @author ruoyi
 */
public class RegisterBody extends LoginBody
{

}
用户注册实体类

2.2.4 service包(记录日志类、登录密码逻辑类、用户登录逻辑类)

  该服务类通过@Component注解注入到Spring容器中,只用于记录用户登录信息。通过创建登录信息对象记载登录用户的用户名、IP、消息,之后通过日志服务调用对象remoteLogService完成用户登录信息日志记录。

package com.ruoyi.auth.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ruoyi.common.core.constant.Constants;
import com.ruoyi.common.core.constant.SecurityConstants;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.ip.IpUtils;
import com.ruoyi.system.api.RemoteLogService;
import com.ruoyi.system.api.domain.SysLogininfor;

/**
 * 记录日志方法
 * 
 * @author ruoyi
 */
@Component
public class SysRecordLogService
{
    @Autowired
    private RemoteLogService remoteLogService;

    /**
     * 记录登录信息
     * 
     * @param username 用户名
     * @param status 状态
     * @param message 消息内容
     * @return
     */
    public void recordLogininfor(String username, String status, String message)
    {
        SysLogininfor logininfor = new SysLogininfor();
        logininfor.setUserName(username);
        logininfor.setIpaddr(IpUtils.getIpAddr());

        logininfor.setMsg(message);
        // 日志状态
        if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
        {
            logininfor.setStatus(Constants.LOGIN_SUCCESS_STATUS);
        }
        else if (Constants.LOGIN_FAIL.equals(status))
        {
            logininfor.setStatus(Constants.LOGIN_FAIL_STATUS);
        }
        remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER);
    }
}
SysRecordLogService

   该服务类通过@Component注解注入到Spring容器中,用于验证用户密码登录。主要通过Redis根据用户名获取Redis缓存的key,通过该key将输入密码错误次数作为值,并设置过期时间缓存在Redis中。验证完成后,清除Redis中的缓存。

 1 package com.ruoyi.auth.service;
 2 
 3 import java.util.concurrent.TimeUnit;
 4 import org.springframework.beans.factory.annotation.Autowired;
 5 import org.springframework.stereotype.Component;
 6 import com.ruoyi.common.core.constant.CacheConstants;
 7 import com.ruoyi.common.core.constant.Constants;
 8 import com.ruoyi.common.core.exception.ServiceException;
 9 import com.ruoyi.common.redis.service.RedisService;
10 import com.ruoyi.common.security.utils.SecurityUtils;
11 import com.ruoyi.system.api.domain.SysUser;
12 
13 /**
14  * 登录密码方法
15  * 
16  * @author ruoyi
17  */
18 @Component
19 public class SysPasswordService
20 {
21     @Autowired
22     private RedisService redisService;
23 
24     private int maxRetryCount = CacheConstants.PASSWORD_MAX_RETRY_COUNT;
25 
26     private Long lockTime = CacheConstants.PASSWORD_LOCK_TIME;
27 
28     @Autowired
29     private SysRecordLogService recordLogService;
30 
31     /**
32      * 登录账户密码错误次数缓存键名
33      * 
34      * @param username 用户名
35      * @return 缓存键key
36      */
37     private String getCacheKey(String username)
38     {
39         return CacheConstants.PWD_ERR_CNT_KEY + username;
40     }
41 
42     public void validate(SysUser user, String password)
43     {
44         String username = user.getUserName();
45 
46         Integer retryCount = redisService.getCacheObject(getCacheKey(username));
47 
48         if (retryCount == null)
49         {
50             retryCount = 0;
51         }
52 
53         if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
54         {
55             String errMsg = String.format("密码输入错误%s次,帐户锁定%s分钟", maxRetryCount, lockTime);
56             recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL,errMsg);
57             throw new ServiceException(errMsg);
58         }
59 
60         if (!matches(user, password))
61         {
62             retryCount = retryCount + 1;
63             recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, String.format("密码输入错误%s次", retryCount));
64             redisService.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
65             throw new ServiceException("用户不存在/密码错误");
66         }
67         else
68         {
69             clearLoginRecordCache(username);
70         }
71     }
72 
73     public boolean matches(SysUser user, String rawPassword)
74     {
75         return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
76     }
77 
78     public void clearLoginRecordCache(String loginName)
79     {
80         if (redisService.hasKey(getCacheKey(loginName)))
81         {
82             redisService.deleteObject(getCacheKey(loginName));
83         }
84     }
85 }
SysPasswordService

该服务类是通过@Component注解注入到Spring容器中,该类包含用户注册、登录、注销流程

  注册流程主要是检验注册用户/密码是否填写、账户长度、密码长度等问题,接着设置用户信息,并通过远程调用RemoteUserService实现用户注册,并将注册信息记录通过SysRecordLogService实例同步到数据库中

  登录流程主要是检验用户/密码是否填写、用户名和密码是否在指定范围、IP是否在黑名单、登录用户是否存在,是否删除、禁用。然后根据用户信息和密码通过调用passwordService验证密码,最后通过SysRecordLogService实例同步登录日志记录到数据库。

  注销流程通过SysRecordLogService实例同步注销日志记录到数据库

SysLoginService

2.2.5 TokenController

  该类主要根据映射路径进行业务处理并返回业务处理结果。其中包含用户注册、登录、刷新token、注销等操作。

 1 package com.ku.auth.controller;
 2 
 3 import com.ku.auth.form.LoginBody;
 4 import com.ku.auth.form.RegisterBody;
 5 import com.ku.auth.service.SysLoginService;
 6 import com.ku.common.core.domain.R;
 7 import com.ku.common.core.utils.JwtUtils;
 8 import com.ku.common.core.utils.StringUtils;
 9 import com.ku.common.security.auth.AuthUtil;
10 import com.ku.common.security.service.TokenService;
11 import com.ku.common.security.utils.SecurityUtils;
12 import com.ku.system.api.model.LoginUser;
13 import org.springframework.beans.factory.annotation.Autowired;
14 import org.springframework.web.bind.annotation.DeleteMapping;
15 import org.springframework.web.bind.annotation.PostMapping;
16 import org.springframework.web.bind.annotation.RequestBody;
17 import org.springframework.web.bind.annotation.RestController;
18 
19 import javax.servlet.http.HttpServletRequest;
20 
21 /**
22  * token控制
23  */
24 @RestController
25 public class TokenController {
26     @Autowired(required = false)
27     private TokenService tokenService;
28 
29     @Autowired(required = false)
30     private SysLoginService sysLoginService;
31 
32     @PostMapping("login")
33     public R<?> login(@RequestBody LoginBody form){
34         //用户登录
35         LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
36         //获取用户token
37         return R.ok(tokenService.createToken(userInfo));
38     }
39 
40     @DeleteMapping("logout")
41     public R<?> logout(HttpServletRequest request){
42         String token = SecurityUtils.getToken(request);
43         if (StringUtils.isNotEmpty(token)){
44             String username = JwtUtils.getUserName(token);
45             //删除用户缓存记录
46             AuthUtil.logoutByToken(token);
47             sysLoginService.logout(username);
48         }
49         return R.ok();
50     }
51 
52     @PostMapping("refresh")
53     public R<?> refresh(HttpServletRequest request){
54         LoginUser loginUser = tokenService.getLoginUser(request);
55         if (StringUtils.isNotNull(loginUser)){
56             //刷新令牌有效期
57             tokenService.refreshToken(loginUser);
58             return R.ok();
59         }
60         return R.ok();
61     }
62 
63     @PostMapping("register")
64     public R<?> register(@RequestBody RegisterBody registerBody){
65         //用户注册
66         sysLoginService.register(registerBody.getUsername(), registerBody.getPassword());
67         return R.ok();
68     }
69 
70 }
TokenController

2.2.6 TokenService

  该类包含创建token,通过request获取token,通过token获取userKey,然后根据拼接后的userKey从Redis缓存中获取登录用户信息、根据登录用户设置用户信息、通过token删除用户、通过登录用户验证token,通过登录用户刷新令牌,通过userKey获取t拼接后的userKey。

  1 package com.ku.common.security.service;
  2 
  3 import com.ku.common.core.constant.CacheConstants;
  4 import com.ku.common.core.constant.SecurityConstants;
  5 import com.ku.common.core.utils.JwtUtils;
  6 import com.ku.common.core.utils.ServletUtils;
  7 import com.ku.common.core.utils.StringUtils;
  8 import com.ku.common.core.utils.ip.IpUtils;
  9 import com.ku.common.core.utils.uuid.IdUtils;
 10 import com.ku.common.redis.service.RedisService;
 11 import com.ku.common.security.utils.SecurityUtils;
 12 import com.ku.system.api.model.LoginUser;
 13 import org.slf4j.Logger;
 14 import org.slf4j.LoggerFactory;
 15 import org.springframework.beans.factory.annotation.Autowired;
 16 import org.springframework.stereotype.Component;
 17 
 18 import javax.servlet.http.HttpServletRequest;
 19 import java.util.HashMap;
 20 import java.util.Map;
 21 import java.util.concurrent.TimeUnit;
 22 
 23 /**
 24  * token验证处理功能
 25  * 1)根据登录用户信息创建令牌;通过map存储token,userId、username;通过map返回token以及过期时间
 26  * 2)通过请求体中的token前缀获取用户身份信息
 27  * 3)根据登录用户刷新令牌有效时间(通过token作为键获取缓存中对应的值,再将该值作为键,loginUser作为值缓存在Redis中,并设置12小时的过期时间)
 28  * @author ruoyi
 29  */
 30 @Component
 31 public class TokenService
 32 {
 33     private static final Logger log = LoggerFactory.getLogger(TokenService.class);
 34 
 35     @Autowired
 36     private RedisService redisService;
 37 
 38     protected static final long MILLIS_SECOND = 1000;
 39 
 40     protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
 41 
 42     private final static long expireTime = CacheConstants.EXPIRATION;
 43 
 44     private final static String ACCESS_TOKEN = CacheConstants.LOGIN_TOKEN_KEY;
 45 
 46     private final static Long MILLIS_MINUTE_TEN = CacheConstants.REFRESH_TIME * MILLIS_MINUTE;
 47 
 48     /**
 49      * 创建令牌
 50      */
 51     public Map<String, Object> createToken(LoginUser loginUser)
 52     {
 53         String token = IdUtils.fastUUID();
 54         Long userId = loginUser.getSysUser().getUserId();
 55         String userName = loginUser.getSysUser().getUserName();
 56         loginUser.setToken(token);
 57         loginUser.setUserid(userId);
 58         loginUser.setUsername(userName);
 59         loginUser.setIpaddr(IpUtils.getIpAddr());
 60         refreshToken(loginUser);
 61 
 62         // Jwt存储信息
 63         Map<String, Object> claimsMap = new HashMap<String, Object>();
 64         claimsMap.put(SecurityConstants.USER_KEY, token);
 65         claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
 66         claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);
 67 
 68         // 接口返回信息
 69         Map<String, Object> rspMap = new HashMap<String, Object>();
 70         rspMap.put("access_token", JwtUtils.createToken(claimsMap));
 71         rspMap.put("expires_in", expireTime);
 72         return rspMap;
 73     }
 74 
 75     /**
 76      * 获取用户身份信息
 77      *
 78      * @return 用户信息
 79      */
 80     public LoginUser getLoginUser()
 81     {
 82         return getLoginUser(ServletUtils.getRequest());
 83     }
 84 
 85     /**
 86      * 根据请求体中的token前缀获取用户身份信息
 87      *
 88      * @return 用户信息
 89      */
 90     public LoginUser getLoginUser(HttpServletRequest request)
 91     {
 92         // 获取请求携带的令牌
 93         String token = SecurityUtils.getToken(request);
 94         return getLoginUser(token);
 95     }
 96 
 97     /**
 98      * 根据token获取用户身份信息
 99      *
100      * @return 用户信息
101      */
102     public LoginUser getLoginUser(String token)
103     {
104         LoginUser user = null;
105         try
106         {
107             if (StringUtils.isNotEmpty(token))
108             {
109                 //通过token获取userKey,再根据字符串拼接后的userkey获取登录用户信息
110                 String userkey = JwtUtils.getUserKey(token);
111                 user = redisService.getCacheObject(getTokenKey(userkey));
112                 return user;
113             }
114         }
115         catch (Exception e)
116         {
117             log.error("获取用户信息异常'{}'", e.getMessage());
118         }
119         return user;
120     }
121 
122     /**
123      * 设置用户身份信息
124      */
125     public void setLoginUser(LoginUser loginUser)
126     {
127         if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
128         {
129             refreshToken(loginUser);
130         }
131     }
132 
133     /**
134      * 删除用户缓存信息
135      */
136     public void delLoginUser(String token)
137     {
138         if (StringUtils.isNotEmpty(token))
139         {
140             String userkey = JwtUtils.getUserKey(token);
141             redisService.deleteObject(getTokenKey(userkey));
142         }
143     }
144 
145     /**
146      * 验证令牌有效期,相差不足120分钟,自动刷新缓存
147      *
148      * @param loginUser
149      */
150     public void verifyToken(LoginUser loginUser)
151     {
152         long expireTime = loginUser.getExpireTime();
153         long currentTime = System.currentTimeMillis();
154         if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
155         {
156             refreshToken(loginUser);
157         }
158     }
159 
160     /**
161      * 刷新令牌有效期
162      * 通过token作为键获取缓存中对应的值,再将该值作为键,loginUser作为值缓存在Redis中,并设置12小时的过期时间
163      *
164      * @param loginUser 登录信息
165      */
166     public void refreshToken(LoginUser loginUser)
167     {
168         loginUser.setLoginTime(System.currentTimeMillis());
169         loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
170         // 根据uuid将loginUser缓存
171         String userKey = getTokenKey(loginUser.getToken());
172         redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
173     }
174 
175     /**
176      * 通过userkey拼接字符串常量获取tokenKey
177      * @param token
178      * @return
179      */
180     private String getTokenKey(String token)
181     {
182         return ACCESS_TOKEN + token;
183     }
184 }
TokenService

2.2.7 权限认证类

  该类封装了一下方法:根据token注销会话,检验用户是否登录(SecurityContextHandler通过ThreadLocal获取用户信息)、验证用户token有效期、检查用户角色、检查用户权限

 1 package com.ku.common.security.auth;
 2 
 3 import com.ku.common.security.annotation.RequiresPermissions;
 4 import com.ku.common.security.annotation.RequiresRoles;
 5 import com.ku.system.api.model.LoginUser;
 6 
 7 /**
 8  * Token权限验证工具类
 9  * 通过AuthUtil类封装调用AuthLogic类中的方法
10  */
11 public class AuthUtil {
12     /**
13      * 封装的AuthLogic对象
14      */
15     public static AuthLogic authLogic = new AuthLogic();
16 
17     //根据token注销用户会话
18     public static void logout(){
19         authLogic.logout();
20     }
21 
22     //根据token注销会话,这个其实在上面的方法中已经用到,不知道这里还重写干啥
23     public static void logoutByToken(String token){
24         authLogic.logoutByToken(token);
25     }
26 
27     //校验当前用户是否已登录,若未登录,则抛出异常
28     public static void checkLogin(){
29         authLogic.checkLogin();
30     }
31 
32     //根据token获取当前用户登录信息
33     //通过token获取userkey,再通过userkey从Redis缓存中后去登录用户信息
34     public static LoginUser getLoginUser(String token){
35         return authLogic.getLoginUser(token);
36     }
37 
38     //验证当前用户有效期
39     public static void verifyLoginUserExpire(LoginUser loginUser){
40         authLogic.verifyLoginUserExpire(loginUser);
41     }
42 
43     //检验当前账号是否含有指定角色标识, 返回true或false(项目中没有使用,这里先不写)
44 
45     //当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException(项目中没有使用,这里先不写)
46 
47     //根据注解传入参数鉴权, 如果验证未通过,则抛出异常: NotRoleException
48     public static void checkRole(RequiresRoles requiresRoles){
49         authLogic.checkRole(requiresRoles);
50     }
51 
52     //当前账号是否含有指定角色标识 [指定多个,必须全部验证通过(项目中没有使用,这里先不写)
53     //当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可](项目中没有使用,这里先不写)
54 
55 
56     public static void checkPermi(RequiresPermissions requiresPermissions){
57         authLogic.checkPermi(requiresPermissions);
58     }
59 
60 }
AuthUtils

2.2.8 权限逻辑实现类

  该类是对上述AuthUtils类方法的实现。

  1 package com.ku.common.security.auth;
  2 
  3 import com.ku.common.core.context.SecurityContextHolder;
  4 import com.ku.common.core.exception.auth.NotLoginException;
  5 import com.ku.common.core.exception.auth.NotPermissionException;
  6 import com.ku.common.core.exception.auth.NotRoleException;
  7 import com.ku.common.core.utils.SpringUtils;
  8 import com.ku.common.core.utils.StringUtils;
  9 import com.ku.common.security.annotation.Logic;
 10 import com.ku.common.security.annotation.RequiresPermissions;
 11 import com.ku.common.security.annotation.RequiresRoles;
 12 import com.ku.common.security.service.TokenService;
 13 import com.ku.common.security.utils.SecurityUtils;
 14 import com.ku.system.api.model.LoginUser;
 15 import org.springframework.util.PatternMatchUtils;
 16 
 17 import javax.security.auth.login.LoginException;
 18 import java.security.Security;
 19 import java.util.Collection;
 20 import java.util.HashSet;
 21 import java.util.Set;
 22 
 23 /**
 24  *
 25  */
 26 public class AuthLogic {
 27     //所有权限标识
 28     private static final String ALL_PERMISSION = "*:*:*";
 29 
 30     //管理员角色权限标识
 31     private static final String SUPER_ADMIN = "admin";
 32 
 33     //通过BeanFactory获取类实例
 34     public TokenService tokenService = SpringUtils.getBean(TokenService.class);
 35 
 36     //注销用户会话
 37     public void logout(){
 38         String token = SecurityUtils.getToken();
 39         if (token == null){
 40             return;
 41         }
 42         logoutByToken(token);
 43     }
 44 
 45     //根据token注销用户会话
 46     public void logoutByToken(String token){
 47         //先通过token从JwtUtils类中获取userkey,
 48         // 再通过userkey获取存储在Redis中的键,再根据键删除Redis缓存中的用户
 49         tokenService.delLoginUser(token);
 50     }
 51 
 52     public void checkLogin(){
 53         getLoginUser();
 54     }
 55 
 56     //检验用户是否已登录,若未登录,抛异常
 57     public LoginUser getLoginUser(){
 58         String token = SecurityUtils.getToken();
 59         if (token ==null){
 60             throw new NotLoginException("未提供token");
 61         }
 62         //通过login_user标识符和类从SecurityContextHandler中获取用户信息
 63         //通过login_user常量作为key和登录用户通过ThreadLocal当前线程变量中获取用户信息
 64         LoginUser loginUser = SecurityUtils.getLoginUser();
 65         if (loginUser == null){
 66             throw new NotLoginException("无效的token");
 67         }
 68         return loginUser;
 69     }
 70 
 71     //获取当前用户缓存信息,若未登录,抛异常
 72     public LoginUser getLoginUser(String token)
 73     {
 74         return tokenService.getLoginUser(token);
 75     }
 76 
 77 
 78      //功能:验证当前用户有效期, 如果相差不足120分钟,自动刷新缓存
 79     //流程:首先获取loginUser中的过期时间,然后比较过期时间与当前时间的时间差
 80     //若小于,则刷新token时间(设置当前登录时间以及过期时间,
 81      // 再根据登录用户的token获取userkey,再通过设置过期时间,userkey作为key,存储登录用户
 82     public void verifyLoginUserExpire(LoginUser loginUser)
 83     {
 84         tokenService.verifyToken(loginUser);
 85     }
 86 
 87     //获取当前账号的角色列表
 88     public Set<String> getRoleList(){
 89         try{
 90             LoginUser loginUser = getLoginUser();
 91             return loginUser.getRoles();
 92         }catch(Exception e){
 93             return new HashSet<>();
 94         }
 95     }
 96 
 97     //根据注解(@RequiresRoles)鉴权(重点)
 98     public void checkRole(RequiresRoles requiresRoles){
 99         if (requiresRoles.logic() == Logic.AND){
100             checkRoleAnd(requiresRoles.value());
101         }else{
102             checkRoleOr(requiresRoles.value());
103         }
104     }
105 
106     //验证用户是否含有指定角色,必须全部拥有
107     //String... roles:可变长参数,可传多个字符串参数作为一个字符数组或集合;
108     // 但只能作为方法的最后一个参数,且只能有一个
109     public void checkRoleAnd(String... roles){
110         Set<String> roleList = getRoleList();
111         for (String role : roles) {
112             if(!hasRole(roleList, role)){
113                 throw new NotRoleException(role);
114             }
115         }
116     }
117 
118     //验证用户是否含有指定角色,只需包含其中一个
119     public void checkRoleOr(String... roles){
120         Set<String> roleList = getRoleList();
121         for (String role : roles) {
122             if (hasRole(roleList, role)){
123                 return;
124             }
125         }
126         if (roles.length > 0){
127             throw new NotRoleException(roles);
128         }
129     }
130 
131     //根据注解(@RequiresPermissions)鉴权, 若验证未通过,则抛异常:NotPermissionException
132     public void checkPermi(RequiresPermissions requiresPermissions){
133         SecurityContextHolder.setPermission(StringUtils.join(requiresPermissions.value(), ","));
134         if (requiresPermissions.logic() == Logic.AND){
135             checkPermiAnd(requiresPermissions.value());
136         }else{
137             checkPermiOr(requiresPermissions.value());
138         }
139     }
140 
141     //获取当前账号的权限列表
142     public Set<String> getPermiList(){
143         try
144         {
145             LoginUser loginUser = getLoginUser();
146             return loginUser.getPermissions();
147         }
148         catch (Exception e)
149         {
150             return new HashSet<>();
151         }
152     }
153 
154     //验证用户是否含有指定权限,必须全部拥有,即只要没有其中一个就抛异常
155     //实现:首先获取登录用户所有权限集合,然后通过for循环比较,如何比较呢?
156     //通过使用stream流来比较,1)判断所有权限是否包含权限集合中元素;
157     // 2)将传入的权限与权限集合中的元素匹配
158     //角色判断同理
159     public  void checkPermiAnd(String... permissions){
160         Set<String> permissionList = getPermiList();
161         for (String permission : permissions) {
162             if (!hasPermi(permissionList, permission)){
163                 throw new NotPermissionException(permission);
164             }
165         }
166     }
167 
168     //验证用户是否含有指定权限,只需包含其中一个,只要有其中一个即可
169     //
170     public  void checkPermiOr(String... permissions){
171         Set<String> permissionList = getPermiList();
172         for (String permission : permissions) {
173             if (hasPermi(permissionList, permission)){
174                 return;
175             }
176            if (permissions.length > 0){
177                throw new NotPermissionException(permissions);
178            }
179         }
180     }
181 
182     //判断是否包含权限
183     public boolean hasPermi(Collection<String> authorities, String permission){
184         return authorities.stream().filter(StringUtils::hasText)
185                 .anyMatch(x -> ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission));
186     }
187 
188     //判断是否包含角色
189     public boolean hasRole(Collection<String> roles, String role){
190         return roles.stream().filter(StringUtils::hasText)
191                 //anyMatch方法会对集合中的元素逐个应用Lambda表达式,如果存在满足条件的元素,则返回true;否则返回false。
192                 //PatternMatchUtils.simpleMatch(x, role):模式匹配
193                 .anyMatch(x -> SUPER_ADMIN.contains(x) || PatternMatchUtils.simpleMatch(x, role));
194     }
195 }
AuthLogic

2.2.9 自定义注解实现角色和权限检验

  在本节中,通过自定义注解,自定义枚举类来实现角色和权限的注解自定义。

  不管是权限还是角色,在系统中都会有所有和其中一种,因此这里定义一个枚举类来实现该功能。

1 package com.ku.common.security.annotation;
2 
3 //权限注解的验证模式
4 public enum Logic {
5     //必须具有所有的元素
6     AND,
7     //只需其中一个元素
8     OR
9 }
AndOrEnum

  自定义权限注解,可用于方法和类上。默认需要所有权限

 1 package com.ku.common.security.annotation;
 2 
 3 //权限认证:必须具有指定权限才能进入该方法
 4 
 5 import java.lang.annotation.ElementType;
 6 import java.lang.annotation.Retention;
 7 import java.lang.annotation.RetentionPolicy;
 8 import java.lang.annotation.Target;
 9 
10 @Retention(RetentionPolicy.RUNTIME)
11 @Target({ElementType.METHOD, ElementType.TYPE})
12 public @interface RequiresPermissions {
13     //需要校验的权限码
14     String[] value() default {};
15 
16     //验证模式:AND / OR,默认AND
17     Logic logic() default Logic.AND;
18 }
@RequiresPermissions
 1 //根据注解(@RequiresPermissions)鉴权, 若验证未通过,则抛异常:NotPermissionException
 2     public void checkPermi(RequiresPermissions requiresPermissions){
 3         SecurityContextHolder.setPermission(StringUtils.join(requiresPermissions.value(), ","));
 4         if (requiresPermissions.logic() == Logic.AND){
 5             checkPermiAnd(requiresPermissions.value());
 6         }else{
 7             checkPermiOr(requiresPermissions.value());
 8         }
 9     }
10 
11     //获取当前账号的权限列表
12     public Set<String> getPermiList(){
13         try
14         {
15             LoginUser loginUser = getLoginUser();
16             return loginUser.getPermissions();
17         }
18         catch (Exception e)
19         {
20             return new HashSet<>();
21         }
22     }
23 
24     //验证用户是否含有指定权限,必须全部拥有,即只要没有其中一个就抛异常
25     //实现:首先获取登录用户所有权限集合,然后通过for循环比较,如何比较呢?
26     //通过使用stream流来比较,1)判断所有权限是否包含权限集合中元素;
27     // 2)将传入的权限与权限集合中的元素匹配
28     //角色判断同理
29     public  void checkPermiAnd(String... permissions){
30         Set<String> permissionList = getPermiList();
31         for (String permission : permissions) {
32             if (!hasPermi(permissionList, permission)){
33                 throw new NotPermissionException(permission);
34             }
35         }
36     }
37 
38     //验证用户是否含有指定权限,只需包含其中一个,只要有其中一个即可
39     //
40     public  void checkPermiOr(String... permissions){
41         Set<String> permissionList = getPermiList();
42         for (String permission : permissions) {
43             if (hasPermi(permissionList, permission)){
44                 return;
45             }
46            if (permissions.length > 0){
47                throw new NotPermissionException(permissions);
48            }
49         }
50     }
51 
52     //判断是否包含权限
53     public boolean hasPermi(Collection<String> authorities, String permission){
54         return authorities.stream().filter(StringUtils::hasText)
55                 .anyMatch(x -> ALL_PERMISSION.contains(x) || PatternMatchUtils.simpleMatch(x, permission));
56     }
@RequiresPermissions实现

  自定义角色权限注解,可用于方法和类上。默认需要所有角色

 1 package com.ku.common.security.annotation;
 2 
 3 import java.lang.annotation.ElementType;
 4 import java.lang.annotation.Retention;
 5 import java.lang.annotation.RetentionPolicy;
 6 import java.lang.annotation.Target;
 7 
 8 //角色认证:必须具有指定角色标识才能进入该方法
 9 @Retention(RetentionPolicy.RUNTIME)
10 @Target({ElementType.METHOD, ElementType.TYPE})//可用于类和方法
11 public @interface RequiresRoles {
12     //需要校验角色标识
13     String[] value() default {};
14 
15     //验证逻辑:AND/OR,默认AND
16     Logic logic() default Logic.AND;
17 }
@RequiresRoles注解

  角色注解实现逻辑

 1 //获取当前账号的角色列表
 2     public Set<String> getRoleList(){
 3         try{
 4             LoginUser loginUser = getLoginUser();
 5             return loginUser.getRoles();
 6         }catch(Exception e){
 7             return new HashSet<>();
 8         }
 9     }
10 
11     //根据注解(@RequiresRoles)鉴权(重点)
12     public void checkRole(RequiresRoles requiresRoles){
13         if (requiresRoles.logic() == Logic.AND){
14             checkRoleAnd(requiresRoles.value());
15         }else{
16             checkRoleOr(requiresRoles.value());
17         }
18     }
19 
20     //验证用户是否含有指定角色,必须全部拥有
21     //String... roles:可变长参数,可传多个字符串参数作为一个字符数组或集合;
22     // 但只能作为方法的最后一个参数,且只能有一个
23     public void checkRoleAnd(String... roles){
24         Set<String> roleList = getRoleList();
25         for (String role : roles) {
26             if(!hasRole(roleList, role)){
27                 throw new NotRoleException(role);
28             }
29         }
30     }
31 
32     //验证用户是否含有指定角色,只需包含其中一个
33     public void checkRoleOr(String... roles){
34         Set<String> roleList = getRoleList();
35         for (String role : roles) {
36             if (hasRole(roleList, role)){
37                 return;
38             }
39         }
40         if (roles.length > 0){
41             throw new NotRoleException(roles);
42         }
43     }
44     //判断是否包含角色
45     public boolean hasRole(Collection<String> roles, String role){
46         return roles.stream().filter(StringUtils::hasText)
47                 //anyMatch方法会对集合中的元素逐个应用Lambda表达式,如果存在满足条件的元素,则返回true;否则返回false。
48                 //PatternMatchUtils.simpleMatch(x, role):模式匹配
49                 .anyMatch(x -> SUPER_ADMIN.contains(x) || PatternMatchUtils.simpleMatch(x, role));
50     }
@RequiresRoles注解实现逻辑

项目链接

RuoYi-Cloud: 🎉 基于Spring Boot、Spring Cloud & Alibaba的分布式微服务架构权限管理系统,同时提供了 Vue3 的版本 (gitee.com)

 

posted @ 2023-08-23 10:59  求知律己  阅读(1174)  评论(0编辑  收藏  举报