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-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-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-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>
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}
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); } }
该服务类通过@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 }
该服务类是通过@Component注解注入到Spring容器中,该类包含用户注册、登录、注销流程。
注册流程主要是检验注册用户/密码是否填写、账户长度、密码长度等问题,接着设置用户信息,并通过远程调用RemoteUserService实现用户注册,并将注册信息记录通过SysRecordLogService实例同步到数据库中。
登录流程主要是检验用户/密码是否填写、用户名和密码是否在指定范围、IP是否在黑名单、登录用户是否存在,是否删除、禁用。然后根据用户信息和密码通过调用passwordService验证密码,最后通过SysRecordLogService实例同步登录日志记录到数据库。
注销流程通过SysRecordLogService实例同步注销日志记录到数据库
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 }
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 }
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 }
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 }
2.2.9 自定义注解实现角色和权限检验
在本节中,通过自定义注解,自定义枚举类来实现角色和权限的注解自定义。
不管是权限还是角色,在系统中都会有所有和其中一种,因此这里定义一个枚举类来实现该功能。
1 package com.ku.common.security.annotation; 2 3 //权限注解的验证模式 4 public enum Logic { 5 //必须具有所有的元素 6 AND, 7 //只需其中一个元素 8 OR 9 }
自定义权限注解,可用于方法和类上。默认需要所有权限
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 }
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 }
自定义角色权限注解,可用于方法和类上。默认需要所有角色
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 }
角色注解实现逻辑
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 }
项目链接
RuoYi-Cloud: 🎉 基于Spring Boot、Spring Cloud & Alibaba的分布式微服务架构权限管理系统,同时提供了 Vue3 的版本 (gitee.com)