项目github地址:https://github.com/liboshuai0...
项目gitee地址:https://gitee.com/liboshuai01...
背景
公司用的项目是基于shiro + cookie/session的,但是现在微服务架构的背景下都是采用token机制进行认证和授权的。于是决定先自己搭建一个spring+shiro+jwt的项目,用来方便替换公司的技术栈。
Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
快速开始
- 搭建一个springboot项目demo
项目
pom.xml
配置文件
父工程pom.xml文件4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.3.RELEASE com.liboshuai mall-tiny 1.0-SNAPSHOT mall-tiny-01 mall-tiny-00-api pom 8 8 3.4.0 1.2.11 8.0.15 1.18.10 1.2.17 2.7.0 2.7.0 2.13.3 2.4.0-SNAPSHOT 4.5.7 0.9.0 3.5.1 2.3 2.4 1.4.0 3.2.0 1.2.58 2.0.4 子工程pom.xml文件
mall-tiny com.liboshuai 1.0-SNAPSHOT 4.0.0 mall-tiny-01 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-data-redis com.baomidou mybatis-plus-boot-starter ${mybatis-plus-boot-starter-version} com.baomidou mybatis-plus-generator ${mybatis-plus-generator-version} org.apache.velocity velocity-engine-core 2.3 org.freemarker freemarker com.alibaba druid-spring-boot-starter ${druid-spring-boot-starte-version} mysql mysql-connector-java ${mysql-connector-java-version} org.projectlombok lombok ${lombok-version} log4j log4j ${log4j-version} io.springfox springfox-swagger2 ${springfox-swagger2-version} io.springfox springfox-swagger-ui ${springfox-swagger-ui-version} cn.hutool hutool-all ${hutool-all-version} org.apache.shiro shiro-spring ${shiro-version} com.auth0 java-jwt ${jwt-version} com.jayway.jsonpath json-path com.alibaba fastjson ${fastjson.version} junit junit 4.13.2 test org.apache.commons commons-text 1.1 commons-io commons-io ${commons-io-version} com.github.xiaoymin knife4j-spring-boot-starter ${knife4j-swagger-version} com.github.binarywang java-testdata-generator 1.1.2 org.springframework.boot spring-boot-maven-plugin 项目配置文件
application.properties
server.port=8081 server.servlet.context-path = /mall-tiny # mysql数据库 spring.datasource.url=jdbc:mysql://81.68.182.114:3307/mall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false spring.datasource.username=ENC(sKYSfpOJ1eQ/GAHhi266/99zGjyWvdaVXar4vpKtLZIjQmb7ZiGn/BuStoWIsPDd) spring.datasource.password=ENC(L97dG07OE0nuqkBm2cQxiOBHSwDd3yrnMPEOU1Ntwaoc8KMHlqe1xycNQZYD6DE7x7y4pmtS9X8NzePxq4toNg==) # redis数据库 spring.redis.host=81.68.216.209 spring.redis.database=0 spring.redis.port=6379 spring.redis.password=ENC(2QRDHOpEQS4c7XGivDuFEsisfC/LbLbAfEFlC3CCH5s1MYr2CPYS+tEJJEsSnMdkm+GeFndZqPSsCx1o3zp5iQ==) spring.redis.timeout=300 spring.redis.jedis.pool.max-active=8 spring.redis.jedis.pool.max-wait=-1ms spring.redis.jedis.pool.max-idle=8 spring.redis.jedis.pool.min-idle=0 mybatis-plus.mapper-locations= classpath:/mapper/*.xml # 手机号验证码key前缀 redis.key.prefix.authCode="portal:authCode:" # 手机验证码超时时间 redis.key.expire.authCode=60 # logback配置文件路径 logging.config=classpath:logback-spring.xml # JWT认证加密私钥(Base64加密) config.encrypt-jwtKey= gHMzjdlP84njamo29YgoAjpH # AccessToken过期时间(秒) config.accessToken-expireTime= 600 # RefreshToken过期时间(秒) 604800秒=7天 config.refreshToken-expireTime= 604800 # Shiro缓存过期时间(秒)(一般设置与AccessToken过期时间一致) config.shiro-cache-expireTime= 600 # 配置mybatis plus逻辑删除 # 全局逻辑删除的实体字段名 mybatis-plus.global-config.db-config.logic-delete-field=isDelete # 逻辑已删除值(默认为 1) mybatis-plus.global-config.db-config.logic-delete-value=1 # 逻辑未删除值(默认为 0) mybatis-plus.global-config.db-config.logic-not-delete-value=0
添加
JwtToken
类,继承AuthenticationToken
/** * @Author: liboshuai * @Date: 2022-09-08 00:53 * @Description: JwtToken 类 */ public class JwtToken implements AuthenticationToken { private static final long serialVersionUID = -8523592214400915953L; private final String token; public JwtToken(String token) { this.token = token; } @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }
添加
JwtUtil
工具类,用来了生成、验证、解析jwt/** * @Author: liboshuai * @Date: 2022-09-09 12:10 * @Description: Jwt工具类 */ @Slf4j @Component public class JwtUtil { private static String ENCRYPT_JWT_KEY_STATIC; private static String ACCESS_TOKEN_EXPIRE_TIME_STATIC; @Value("${config.encrypt-jwtKey}") private String ENCRYPT_JWT_KEY; @Value("${config.accessToken-expireTime}") private String ACCESS_TOKEN_EXPIRE_TIME; /** * 效验token是否正确 */ public static boolean verify(String token) { try { String secret = Base64Util.decode(ENCRYPT_JWT_KEY_STATIC); Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier jwtVerifier = JWT.require(algorithm).build(); jwtVerifier.verify(token); return true; } catch (UnsupportedEncodingException e) { log.error("token认证失败异常:{}", e.getMessage()); e.printStackTrace(); } return false; } /** * 获取Jwt payload的内容 */ public static String getClaim(String token, String claim) { try { // 只能输出String类型,如果是其他类型则返回null return JWT.decode(token).getClaim(claim).asString(); } catch (JWTDecodeException e) { log.error("解密token中的公共信息异常:{}" + e.getMessage()); e.printStackTrace(); } return null; } /** * 生成Jwt */ public static String generateJwt(String username, String currentTimeMillis) { try { // 获取jwt过期时间(单位为毫秒) Date expireDate = new Date(System.currentTimeMillis() + Long.parseLong(ACCESS_TOKEN_EXPIRE_TIME_STATIC) * 1000); // 获取签名 String secret = Base64Util.decode(ENCRYPT_JWT_KEY_STATIC); Algorithm algorithm = Algorithm.HMAC256(secret); // 生成Jwt return JWT.create() // 存放username .withClaim(ShiroConstant.USERNAME, username) // 存放当前时间戳 .withClaim(ShiroConstant.CURRENT_TIME_MILLIS, currentTimeMillis) .withExpiresAt(expireDate) .sign(algorithm); } catch (UnsupportedEncodingException e) { log.error("token生成失败异常:{}", e.getMessage()); e.printStackTrace(); } return null; } @PostConstruct private void init() { ENCRYPT_JWT_KEY_STATIC = ENCRYPT_JWT_KEY; ACCESS_TOKEN_EXPIRE_TIME_STATIC = ACCESS_TOKEN_EXPIRE_TIME; } }
编写我们自定义的JwtFilter,用于加入后面的shiro中
/** * @Author: liboshuai * @Date: 2022-09-09 22:49 * @Description: jwt过滤器 */ @Slf4j public class JwtFilter extends BasicHttpAuthenticationFilter { private static String serverServletContextPath; private static String refreshTokenExpireTime; private final AntPathMatcher pathMatcher = new AntPathMatcher(); @Autowired private RedisClient redis; public JwtFilter() { ResourceBundle resource = ResourceBundle.getBundle("application"); serverServletContextPath = resource.getString("server.servlet.context-path"); refreshTokenExpireTime = resource.getString("config.refreshToken-expireTime"); } /** * 登录认证 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest httpServletRequest = (HttpServletRequest) request; // 添加免登录接口 if (secretFree(httpServletRequest)) { return true; } // 判断用户是否想要登入 if (this.isLoginAttempt(request, response)) { try { // 进行Shiro的登录UserRealm this.executeLogin(request, response); } catch (Exception e) { // 认证出现异常,传递错误信息msg String msg = e.getMessage(); // 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常)) Throwable throwable = e.getCause(); if (throwable instanceof SignatureVerificationException) { // 该异常为JWT的AccessToken认证失败(Token或者密钥不正确) msg = "token或者密钥不正确(" + throwable.getMessage() + ")"; } else if (throwable instanceof TokenExpiredException) { // 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新 if (this.refreshToken(request, response)) { return true; } else { msg = "token已过期(" + throwable.getMessage() + ")"; } } else { // 应用异常不为空 if (throwable != null) { // 获取应用异常msg msg = throwable.getMessage(); } } /** * 错误两种处理方式 1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息 2. * 无需转发,直接返回Response信息 一般使用第二种(更方便) */ // 直接返回Response信息 this.response401(request, response, msg); return false; } } return true; } /** * 添加免密登录路径 */ private boolean secretFree(HttpServletRequest httpServletRequest) { String[] anonUrl = {"/register", "/login", "/swagger-ui.html", "/doc.html", "/webjars/**", "/swagger-resources", "/v2/api-docs", "/swagger-resources/**"}; boolean match = false; String requestURI = httpServletRequest.getRequestURI(); for (String u : anonUrl) { if (pathMatcher.match(serverServletContextPath + u, requestURI)) { match = true; } } return match; } /** * 这里我们详细说明下为什么重写 可以对比父类方法,只是将executeLogin方法调用去除了 * 如果没有去除将会循环调用doGetAuthenticationInfo方法 */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { this.sendChallenge(request, response); return false; } /** * 检测Header里面是否包含Authorization字段,有就进行Token登录认证授权 */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { // HttpServletRequest req = (HttpServletRequest) request; // String authorization = req.getHeader("Authorization"); // return authorization != null; return true; } /** * 进行AccessToken登录认证授权 */ @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = (HttpServletRequest) request; String authorization = req.getHeader(ShiroConstant.AUTHORIZATION); JwtToken token = new JwtToken(authorization); // 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获 this.getSubject(request, response).login(token); // 如果没有抛出异常则代表登入成功,返回true return true; } /** * 刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问 */ private boolean refreshToken(ServletRequest request, ServletResponse response) { // 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现) // String token = this.getAuthzHeader(request); HttpServletRequest req = (HttpServletRequest) request; String token = req.getHeader(ShiroConstant.AUTHORIZATION); // 获取当前Token的帐号信息 String account = JwtUtil.getClaim(token, ShiroConstant.USERNAME); // 判断Redis中RefreshToken是否存在 if (redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) { // Redis中RefreshToken还存在,获取RefreshToken的时间戳 String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString(); // 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新 if (Objects.equals(JwtUtil.getClaim(token, ShiroConstant.CURRENT_TIME_MILLIS), currentTimeMillisRedis)) { // 获取当前最新时间戳 String currentTimeMillis = String.valueOf(System.currentTimeMillis()); // 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性) redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis, Integer.parseInt(refreshTokenExpireTime)); // 刷新AccessToken,设置时间戳为当前最新时间戳 token = JwtUtil.generateJwt(account, currentTimeMillis); // 将新刷新的AccessToken再次进行Shiro的登录 JwtToken jwtToken = new JwtToken(token); // 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true this.getSubject(request, response).login(jwtToken); // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回 HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader(ShiroConstant.AUTHORIZATION, token); httpServletResponse.setHeader(ShiroConstant.ACCESS_CONTROL_EXPOSE_HEADERS, ShiroConstant.AUTHORIZATION); return true; } } return false; } /** * 无需转发,直接返回Response信息 */ private void response401(ServletRequest req, ServletResponse resp, String msg) { HttpServletResponse httpServletResponse = (HttpServletResponse) resp; httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value()); httpServletResponse.setCharacterEncoding(CharsetUtil.UTF_8); httpServletResponse.setContentType(ShiroConstant.CONTENT_TYPE); PrintWriter out = null; try { out = httpServletResponse.getWriter(); String data = JSONObject.toJSONString(ResponseResult.fail(ResponseCode.NOT_LOGIN_IN, msg)); out.append(data); } catch (IOException e) { throw new CustomException("直接返回Response信息出现IOException异常:" + e.getMessage()); } finally { if (out != null) { out.close(); } } } /** * 对跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } }
编写自定义的ShiroRealm-
UserRealm
/** * @Author: liboshuai * @Date: 2022-09-08 01:17 * @Description: 自定义shiroRealm */ @Slf4j @Component public class UserRealm extends AuthorizingRealm { @Autowired private RedisClient redis; @Autowired private UmsAdminService umsAdminService; @Autowired private UmsRoleService umsRoleService; @Autowired private UmsPermissionService umsPermissionService; /** * 大坑!,必须重写此方法,不然Shiro会报错 */ @Override public boolean supports(AuthenticationToken token) { return token instanceof JwtToken; } /** * 授权认证 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 从token中获取username String username = JwtUtil.getClaim(principalCollection.toString(), ShiroConstant.USERNAME); // 根据用户名称获取角色名称集合 List
umsRoleDTOList = umsRoleService.findRolesByUsername(username); Set roleNameSet = umsRoleDTOList.stream().map(UmsRoleDTO::getName).collect(Collectors.toSet()); // 根据角色id集合获取权限值集合 List userIdList = umsRoleDTOList.stream().map(UmsRoleDTO::getId).collect(Collectors.toList()); List permissionList = umsPermissionService.findPermissionsByRoleIds(userIdList); Set permissionValueSet = permissionList.stream().map(UmsPermissionDTO::getValue).collect(Collectors.toSet()); // 将角色名称集合和权限值集合放入到shiro认证信息中 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roleNameSet); simpleAuthorizationInfo.setStringPermissions(permissionValueSet); return simpleAuthorizationInfo; } /** * 登录认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 获取token信息 String token = (String) authenticationToken.getCredentials(); if (StringUtils.isBlank(token)) { throw new AuthenticationException(ShiroConstant.TOKEN_CANNOT_BE_EMPTY); } // 使用jwtUtil解密获取Username String username = JwtUtil.getClaim(token, ShiroConstant.USERNAME); if (StringUtils.isBlank(username)) { throw new AuthenticationException(ShiroConstant.TOKEN_INVALID); } Long userId = umsAdminService.findUserIdByUserName(username); if (Objects.isNull(userId)) { throw new AuthenticationException(ShiroConstant.USER_DIDNT_EXISTED); } // 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致 if (JwtUtil.verify(token) && redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username)) { // 获取RefreshToken的时间戳 String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username).toString(); // 获取AccessToken时间戳,与RefreshToken的时间戳对比 if (Objects.equals(JwtUtil.getClaim(token, ShiroConstant.CURRENT_TIME_MILLIS), currentTimeMillisRedis)) { return new SimpleAuthenticationInfo(token, token, ShiroConstant.REALM_NAME); } } throw new AuthenticationException(ShiroConstant.TOKEN_EXPIRED_OR_INCORRECT); } } 编写Redis相关代码,用于替换shiro自带的缓存
CustomCache/** * @Author: liboshuai * @Date: 2022-09-12 19:20 * @Description: 重写Shiro的Cache保存读取 */ @Component public class CustomCache
implements Cache { @Value("${config.accessToken-expireTime}") private String ACCESS_TOKEN_EXPIRE_TIME; private final RedisTemplate redisTemplate; // todo: 如果jwt的缓存除了问题,可能需要去除这里的@Autowired @Autowired public CustomCache(RedisTemplate redisTemplate) { // 使用StringRedisSerializer做序列化 // redisTemplate.setValueSerializer(new StringRedisSerializer()); this.redisTemplate = redisTemplate; } /** * 缓存的key名称获取为shiro:cache:account * * @param key * @return java.lang.String * @author Wang926454 * @date 2018/9/4 18:33 */ private String getKey(Object key) { return RedisConstant.PREFIX_SHIRO_CACHE + JwtUtil.getClaim(key.toString(), ShiroConstant.USERNAME); } /** * 获取缓存 */ @Override public Object get(Object key) throws CacheException { return redisTemplate.opsForValue().get(this.getKey(key)); } /** * 保存缓存 */ @Override public Object put(Object key, Object value) throws CacheException { // 读取配置文件,获取Redis的Shiro缓存过期时间 // PropertiesUtil.readProperties("config.properties"); // String shiroCacheExpireTime = // PropertiesUtil.getProperty("shiroCacheExpireTime"); // 设置Redis的Shiro缓存 try { redisTemplate.opsForValue().set(this.getKey(key), value, Integer.parseInt(ACCESS_TOKEN_EXPIRE_TIME), TimeUnit.SECONDS); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除缓存 */ @Override public Object remove(Object key) throws CacheException { redisTemplate.delete(this.getKey(key)); return null; } /** * 清空所有缓存 */ @Override public void clear() throws CacheException { // TODO Auto-generated method stub } /** * 缓存的个数 */ @Override public Set keys() { // TODO Auto-generated method stub return null; } /** * 获取所有的key */ @Override public int size() { // TODO Auto-generated method stub return 0; } /** * 获取所有的value */ @Override public Collection values() { // TODO Auto-generated method stub return null; } } CustomCacheManager
/** * @Author: liboshuai * @Date: 2022-09-12 19:27 * @Description: 重写Shiro缓存管理器 */ public class CustomCacheManager implements CacheManager { private final RedisTemplate
redisTemplate; public CustomCacheManager(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public Cache getCache(String s) throws CacheException { return new CustomCache (redisTemplate); } } RedisClient
/** * @Author: liboshuai * @Date: 2022-09-12 19:38 * @Description: */ @Component public class RedisClient { @Autowired private RedisTemplate
redisTemplate; public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } // =============================common============================ /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } // ============================String============================= /** * 普通缓存获取 * * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * * @param key 键 * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * HashGet * * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * * @param key 键 * @return 对应的多个键值 */ public Map RedisConfig
/** * @Author: liboshuai * @Date: 2022-09-12 19:43 * @Description: Redis缓存配置 */ @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean @Override public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); if (params != null && params.length > 0 && params[0] != null) { for (Object obj : params) { sb.append(obj.toString()); } } return sb.toString(); } }; } /** * RedisTemplate */ @Bean @SuppressWarnings("all") public RedisTemplate
redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate (); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } } 编写自定义异常类
CustomException
/** * @Author: liboshuai * @Date: 2022-09-10 00:30 * @Description: 自定义异常类 */ public class CustomException extends RuntimeException { private static final long serialVersionUID = 781776451227176519L; public CustomException(String msg) { super(msg); } public CustomException() { super(); } }
编写全局异常增加类
ExceptionAdvice
/** * @Author: liboshuai * @Date: 2022-09-10 00:34 * @Description: 异常捕捉增强类 */ @Slf4j @RestControllerAdvice public class ExceptionAdvice { /** * 捕捉所有shiro异常 */ @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(ShiroException.class) public ResponseResult> handle401(ShiroException e) { return ResponseResult.fail(ResponseCode.UNAUTHORIZED, e.getMessage()); } /** * 单独捕捉Shiro(UnauthorizedException)异常 * 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常 */ @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(UnauthorizedException.class) public ResponseResult> handle401(UnauthorizedException e) { return ResponseResult.fail(ResponseCode.UNAUTHORIZED, "无权访问(Unauthorized):当前Subject没有此请求所需权限(" + e.getMessage() + ")"); } /** * 单独捕捉Shiro(UnauthenticatedException)异常 * 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常 */ @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(UnauthenticatedException.class) public ResponseResult> handle401(UnauthenticatedException e) { return ResponseResult.fail(ResponseCode.UNAUTHORIZED, "无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)"); } /** * 获取效验错误信息 */ private Map
getValidError(List fieldErrors) { Map map = new HashMap<>(16); List errorList = new ArrayList<>(); StringBuffer errorMsg = new StringBuffer("效验异常(ValidException):"); for (FieldError error : fieldErrors) { errorList.add(error.getField() + "-" + error.getDefaultMessage()); errorMsg.append(error.getField() + "-" + error.getDefaultMessage() + "-"); } map.put("errorList", errorList); map.put("errorMsg", errorMsg); return map; } } 编写shiro配置类
ShiroConfig
/** * @Author: liboshuai * @Date: 2022-09-09 17:41 * @Description: shiro配置类 */ @Slf4j @Configuration public class ShiroConfig { /** * 配置使用自定义Realm */ @Bean("securityManager") public DefaultWebSecurityManager securityManager(UserRealm userRealm, RedisTemplate
template) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 使用自定义Realm securityManager.setRealm(userRealm); // 关闭Shiro自带的session(因为我们采用的是Jwt token的机制) DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); defaultSubjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(defaultSubjectDAO); // 设置自定义Cache缓存 securityManager.setCacheManager(new CustomCacheManager(template)); return securityManager; } /** * 配置自定义过滤器 */ @Bean("shiroFilter") public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); // 添加自己的过滤器名为jwtFilter Map filterMap = new HashMap<>(16); filterMap.put("jwtFilter", jwtFilterBean()); factoryBean.setFilters(filterMap); factoryBean.setSecurityManager(defaultWebSecurityManager); // 设置无权限时跳转的 url; factoryBean.setUnauthorizedUrl("/unauthorized/无权限"); // 自定义url规则 HashMap filterRuleMap = new HashMap<>(16); // 所有请求通过我们自己的JwtFilter filterRuleMap.put("/**", "jwtFilter"); factoryBean.setFilterChainDefinitionMap(filterRuleMap); return factoryBean; } /** * * 注入bean,此处应注意: * * (1)代码顺序,应放置于shiroFilter后面,否则报错: * * (2)如不在此注册,在filter中将无法正常注入bean *
*/ @Bean("jwtFilter") public JwtFilter jwtFilterBean() { return new JwtFilter(); } /** * 添加注解支持 */ @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); // 强制使用cglib,防止重复代理和可能引起代理出错的问题,https://zhuanlan.zhihu.com/p/29161098 defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * ||启动shiro的apo|| * 使得我们后面加在方法上面的权限控制注解可以生效。 * 例如:@RequiresPermissions("/sys/bank/delete"), @RequiresRoles("admin") */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( DefaultWebSecurityManager defaultWebSecurityManager ) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(defaultWebSecurityManager); return advisor; } }用户注册、登录、退出接口
LoginAdminController
/** * @Author: liboshuai * @Date: 2022-09-10 01:27 * @Description: 用户登录controller */ @Api(tags = "用户登录入口", value = "LoginAdminController") @Slf4j @RestController public class LoginAdminController { @Value("${config.refreshToken-expireTime}") private String refreshTokenExpireTime; @Autowired private RedisClient redis; @Autowired private HttpServletRequest request; @Autowired private UmsAdminService umsAdminService; /** * 用户注册 */ @ApiOperation(value = "注册", httpMethod = "POST") @PostMapping("/register") public ResponseResult> register(@RequestBody UmsAdminVo umsAdminVo) { UmsAdminDTO umsAdminDTO = new UmsAdminDTO(); BeanUtils.copyProperties(umsAdminVo, umsAdminDTO); String username = umsAdminDTO.getUsername(); String password = umsAdminDTO.getPassword(); if (Objects.nonNull(password)) { int saltCount = ShiroConstant.HASH_INTERATIONS; String salt = ByteSource.Util.bytes(username).toString(); String enPassword = new SimpleHash(ShiroConstant.ALGORITH_NAME, password, salt, saltCount).toString(); umsAdminDTO.setPassword(enPassword); umsAdminDTO.setSalt(salt); umsAdminDTO.setSaltCount(saltCount); } umsAdminDTO.setStatus(UserStatusEnum.Enable.getCode()); UmsAdmin umsAdmin = new UmsAdmin(); BeanUtils.copyProperties(umsAdminDTO, umsAdmin); umsAdminService.save(umsAdmin); return ResponseResult.success("注册成功"); } /** * 用户登录 */ @ApiOperation(value = "登录", httpMethod = "POST") @PostMapping("/login") public ResponseResult> login(@RequestParam String username, @RequestParam String password, HttpServletResponse response) { if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { return ResponseResult.fail(ResponseCode.USERNAME_PASSWORD_NULL); } UmsAdminDTO umsAdminDTO = umsAdminService.findByUserName(username); if (Objects.isNull(umsAdminDTO)) { return ResponseResult.fail(ResponseCode.INCORRECT_CREDENTIALS); } if (Objects.isNull(umsAdminDTO.getSalt()) || Objects.isNull(umsAdminDTO.getSaltCount())) { return ResponseResult.fail(ResponseCode.SALT_IS_NOT_EXISTED); } String enPassword = new SimpleHash(ShiroConstant.ALGORITH_NAME, password, umsAdminDTO.getSalt(), umsAdminDTO.getSaltCount()).toString(); if (!Objects.equals(umsAdminDTO.getPassword(), enPassword)) { return ResponseResult.fail(ResponseCode.INCORRECT_CREDENTIALS); } // 清除可能存在的shiro权限信息缓存 if (redis.hasKey(RedisConstant.PREFIX_SHIRO_CACHE + username)) { redis.del(RedisConstant.PREFIX_SHIRO_CACHE + username); } // 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken) String currentTimeMillis = String.valueOf(System.currentTimeMillis()); redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username, currentTimeMillis, Integer.parseInt(refreshTokenExpireTime)); // 从Header中Authorization返回AccessToken,时间戳为当前时间戳 String token = JwtUtil.generateJwt(username, currentTimeMillis); response.setHeader(ShiroConstant.AUTHORIZATION, token); response.setHeader(ShiroConstant.ACCESS_CONTROL_EXPOSE_HEADERS, ShiroConstant.AUTHORIZATION); // 更新登录时间 umsAdminDTO.setLoginTime(LocalDateTime.now()); LambdaUpdateWrapper
umsAdminLambdaUpdateWrapper = new LambdaUpdateWrapper<>(); umsAdminLambdaUpdateWrapper.eq(UmsAdmin::getId, umsAdminDTO.getId()); umsAdminLambdaUpdateWrapper.set(UmsAdmin::getLoginTime, umsAdminDTO.getLoginTime()); umsAdminService.update(umsAdminLambdaUpdateWrapper); return ResponseResult.success("登录成功"); } /** * 退出 */ @ApiOperation(value = "退出", httpMethod = "POST") @PostMapping("/logout") public ResponseResult> logout() { try { String token = ""; // 获取头部信息 Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = headerNames.nextElement(); if (ShiroConstant.AUTHORIZATION.equalsIgnoreCase(key)) { token = request.getHeader(key); } } // 效验 token if (StringUtils.isBlank(token)) { return ResponseResult.fail(ResponseCode.FAILED); } String username = JwtUtil.getClaim(token, ShiroConstant.USERNAME); if (StringUtils.isBlank(username)) { return ResponseResult.fail(ResponseCode.TOKEN_EXPIRE_OR_ERROR, ResponseCode.FAILED.getMessage()); } // 清除shiro权限信息缓存 if (redis.hasKey(RedisConstant.PREFIX_SHIRO_CACHE + username)) { redis.del(RedisConstant.PREFIX_SHIRO_CACHE + username); } // 清除RefreshToken redis.del(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + username); return ResponseResult.success(); } catch (Exception e) { e.printStackTrace(); return ResponseResult.fail(ResponseCode.FAILED, e.getMessage()); } } } 文章参考:https://blog.csdn.net/hd24360...