• Springboot整合Shiro+JWT实现认证授权


    1.Shiro用来认证用户及权限控制,jwt用来生成一个token令牌,暂存用户信息。令牌存储在客户端,用户每次请求将其放在header中,在每个服务器节点进行验证。

    2.导入依赖库:

    1. <dependency>
    2. <groupId>org.apache.shirogroupId>
    3. <artifactId>shiro-webartifactId>
    4. <version>1.5.3version>
    5. dependency>
    6. <dependency>
    7. <groupId>org.apache.shirogroupId>
    8. <artifactId>shiro-springartifactId>
    9. <version>1.5.3version>
    10. dependency>
    11. <dependency>
    12. <groupId>com.auth0groupId>
    13. <artifactId>java-jwtartifactId>
    14. <version>3.10.3version>
    15. dependency>
    16. <dependency>
    17. <groupId>org.springframework.bootgroupId>
    18. <artifactId>spring-boot-configuration-processorartifactId>
    19. <optional>trueoptional>
    20. dependency>
    21. <dependency>
    22. <groupId>org.apache.commonsgroupId>
    23. <artifactId>commons-lang3artifactId>
    24. <version>3.11version>
    25. dependency>
    26. <dependency>
    27. <groupId>org.apache.httpcomponentsgroupId>
    28. <artifactId>httpcoreartifactId>
    29. <version>4.4.13version>
    30. dependency>
    31. <dependency>
    32. <groupId>org.springframework.bootgroupId>
    33. <artifactId>spring-boot-starter-aopartifactId>
    34. dependency>

    3.定义密钥和过期时间

    这个过程一般在springboot配置文件中进行编辑,然后再注入到Javabean中,维护起来方便。

    1. emos:
    2. jwt:
    3. #密钥
    4. secret: abc123456
    5. #令牌过期时间(天)
    6. expire: 5
    7. #令牌缓存时间(天数)
    8. cache-expire: 10

    4.创建JWT工具类

    可以创建一个shiro包,然后再创建类,该类主要方法包括创建token,获得token用户id,验证token

    1. package com.example.emos.wx.config.shiro;
    2. import cn.hutool.core.date.DateField;
    3. import cn.hutool.core.date.DateUtil;
    4. import com.auth0.jwt.JWT;
    5. import com.auth0.jwt.JWTCreator;
    6. import com.auth0.jwt.JWTVerifier;
    7. import com.auth0.jwt.algorithms.Algorithm;
    8. import com.auth0.jwt.interfaces.DecodedJWT;
    9. import com.example.emos.wx.exception.EmosException;
    10. import lombok.extern.slf4j.Slf4j;
    11. import org.springframework.beans.factory.annotation.Value;
    12. import org.springframework.stereotype.Component;
    13. import java.util.Date;
    14. @Component
    15. @Slf4j
    16. public class JwtUtil {
    17. //密钥
    18. @Value("${emos.jwt.secret}")
    19. private String secret;
    20. //过期时间(天)
    21. @Value("${emos.jwt.expire}")
    22. private int expire;
    23. public String createToken(int userId) {
    24. Date date = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire).toJdkDate();
    25. Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象
    26. JWTCreator.Builder builder = JWT.create();
    27. String token = builder.withClaim("userId", userId).withExpiresAt(date).sign(algorithm);
    28. return token;
    29. }
    30. public int getUserId(String token) {
    31. try {
    32. DecodedJWT jwt = JWT.decode(token);
    33. return jwt.getClaim("userId").asInt();
    34. } catch (Exception e) {
    35. throw new EmosException("令牌无效");
    36. }
    37. }
    38. public void verifierToken(String token) {
    39. Algorithm algorithm = Algorithm.HMAC256(secret); //创建加密算法对象
    40. JWTVerifier verifier = JWT.require(algorithm).build();
    41. verifier.verify(token);
    42. }
    43. }

    5.把令牌封装成认证对象

    因为Shiro框架认证需要用到认证对象,把令牌字符串做简单的封装。客户端提交的token不能直接交给Shiro框架,需要向封装成AuthenticationToken类型的对象。

    1. package com.example.emos.wx.config.shiro;
    2. import org.apache.shiro.authc.AuthenticationToken;
    3. public class OAuth2Token implements AuthenticationToken {
    4. private String token;
    5. public OAuth2Token(String token){
    6. this.token = token;
    7. }
    8. @Override
    9. public Object getPrincipal() {
    10. return token;
    11. }
    12. @Override
    13. public Object getCredentials() {
    14. return token;
    15. }
    16. }

    6.创建OAuth2Realm类,实现AuthorizingRealm接口

    1. package com.example.emos.wx.config.shiro;
    2. import org.apache.shiro.authc.*;
    3. import org.apache.shiro.authz.AuthorizationInfo;
    4. import org.apache.shiro.authz.SimpleAuthorizationInfo;
    5. import org.apache.shiro.realm.AuthorizingRealm;
    6. import org.apache.shiro.subject.PrincipalCollection;
    7. import org.springframework.beans.factory.annotation.Autowired;
    8. import org.springframework.stereotype.Component;
    9. import java.util.Set;
    10. @Component
    11. public class OAuth2Realm extends AuthorizingRealm {
    12. @Autowired
    13. private JwtUtil jwtUtil;
    14. @Override
    15. public boolean supports(AuthenticationToken token) {
    16. return token instanceof OAuth2Token;
    17. }
    18. /**
    19. * 授权(验证权限时调用)
    20. */
    21. @Override
    22. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    23. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    24. //TODO 查询用户的权限列表
    25. //TODO 把权限列表添加到info对象中
    26. return info;
    27. }
    28. /**
    29. * 认证(登录时调用)
    30. */
    31. @Override
    32. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    33. //TODO 从令牌中获取userId,然后检测该账户是否被冻结。
    34. SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
    35. //TODO 往info对象中添加用户信息、Token字符串
    36. return info;
    37. }
    38. }

    7.刷新令牌如何设计?

    一个是采用短令牌和长令牌的双令牌机制;一个是采用redis缓存令牌机制。更常用第二种;

     

     

     8.创建ThreadLocalToken类

    1. package com.example.emos.wx.config.shiro;
    2. import org.springframework.stereotype.Component;
    3. @Component
    4. public class ThreadLocalToken {
    5. private ThreadLocal local=new ThreadLocal();
    6. public void setToken(String token){
    7. local.set(token);
    8. }
    9. public String getToken(){
    10. return (String) local.get();
    11. }
    12. public void clear(){
    13. local.remove();
    14. }
    15. }

    9.创建OAuth2Filter类

     

    1. emos:
    2. jwt:
    3. #密钥
    4. secret: abc123456
    5. #令牌过期时间(天)
    6. expire: 5
    7. #令牌缓存时间(天数)
    8. cache-expire: 10
    1. package com.example.emos.wx.config.shiro;
    2. import com.auth0.jwt.exceptions.JWTDecodeException;
    3. import com.auth0.jwt.exceptions.TokenExpiredException;
    4. import org.apache.commons.lang3.StringUtils;
    5. import org.apache.http.HttpStatus;
    6. import org.apache.shiro.authc.AuthenticationException;
    7. import org.apache.shiro.authc.AuthenticationToken;
    8. import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
    9. import org.springframework.beans.factory.annotation.Autowired;
    10. import org.springframework.beans.factory.annotation.Value;
    11. import org.springframework.context.annotation.Scope;
    12. import org.springframework.data.redis.core.RedisTemplate;
    13. import org.springframework.stereotype.Component;
    14. import org.springframework.web.bind.annotation.RequestMethod;
    15. import javax.servlet.FilterChain;
    16. import javax.servlet.ServletException;
    17. import javax.servlet.ServletRequest;
    18. import javax.servlet.ServletResponse;
    19. import javax.servlet.http.HttpServletRequest;
    20. import javax.servlet.http.HttpServletResponse;
    21. import java.io.IOException;
    22. import java.util.concurrent.TimeUnit;
    23. @Component
    24. @Scope("prototype")
    25. public class OAuth2Filter extends AuthenticatingFilter {
    26. @Autowired
    27. private ThreadLocalToken threadLocalToken;
    28. @Value("${emos.jwt.cache-expire}")
    29. private int cacheExpire;
    30. @Autowired
    31. private JwtUtil jwtUtil;
    32. @Autowired
    33. private RedisTemplate redisTemplate;
    34. /**
    35. * 拦截请求之后,用于把令牌字符串封装成令牌对象
    36. */
    37. @Override
    38. protected AuthenticationToken createToken(ServletRequest request,
    39. ServletResponse response) throws Exception {
    40. //获取请求token
    41. String token = getRequestToken((HttpServletRequest) request);
    42. if (StringUtils.isBlank(token)) {
    43. return null;
    44. }
    45. return new OAuth2Token(token);
    46. }
    47. /**
    48. * 拦截请求,判断请求是否需要被Shiro处理
    49. */
    50. @Override
    51. protected boolean isAccessAllowed(ServletRequest request,
    52. ServletResponse response, Object mappedValue) {
    53. HttpServletRequest req = (HttpServletRequest) request;
    54. // Ajax提交application/json数据的时候,会先发出Options请求
    55. // 这里要放行Options请求,不需要Shiro处理
    56. if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
    57. return true;
    58. }
    59. // 除了Options请求之外,所有请求都要被Shiro处理
    60. return false;
    61. }
    62. /**
    63. * 该方法用于处理所有应该被Shiro处理的请求
    64. */
    65. @Override
    66. protected boolean onAccessDenied(ServletRequest request,
    67. ServletResponse response) throws Exception {
    68. HttpServletRequest req = (HttpServletRequest) request;
    69. HttpServletResponse resp = (HttpServletResponse) response;
    70. resp.setHeader("Content-Type", "text/html;charset=UTF-8");
    71. //允许跨域请求
    72. resp.setHeader("Access-Control-Allow-Credentials", "true");
    73. resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
    74. threadLocalToken.clear();
    75. //获取请求token,如果token不存在,直接返回401
    76. String token = getRequestToken((HttpServletRequest) request);
    77. if (StringUtils.isBlank(token)) {
    78. resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
    79. resp.getWriter().print("无效的令牌");
    80. return false;
    81. }
    82. try {
    83. jwtUtil.verifierToken(token); //检查令牌是否过期
    84. } catch (TokenExpiredException e) {
    85. //客户端令牌过期,查询Redis中是否存在令牌,如果存在令牌就重新生成一个令牌给客户端
    86. if (redisTemplate.hasKey(token)) {
    87. redisTemplate.delete(token);//删除令牌
    88. int userId = jwtUtil.getUserId(token);
    89. token = jwtUtil.createToken(userId); //生成新的令牌
    90. //把新的令牌保存到Redis中
    91. redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
    92. //把新令牌绑定到线程
    93. threadLocalToken.setToken(token);
    94. } else {
    95. //如果Redis不存在令牌,让用户重新登录
    96. resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
    97. resp.getWriter().print("令牌已经过期");
    98. return false;
    99. }
    100. } catch (JWTDecodeException e) {
    101. resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
    102. resp.getWriter().print("无效的令牌");
    103. return false;
    104. }
    105. boolean bool = executeLogin(request, response);
    106. return bool;
    107. }
    108. @Override
    109. protected boolean onLoginFailure(AuthenticationToken token,
    110. AuthenticationException e, ServletRequest request, ServletResponse response) {
    111. HttpServletRequest req = (HttpServletRequest) request;
    112. HttpServletResponse resp = (HttpServletResponse) response;
    113. resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
    114. resp.setContentType("application/json;charset=utf-8");
    115. resp.setHeader("Access-Control-Allow-Credentials", "true");
    116. resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
    117. try {
    118. resp.getWriter().print(e.getMessage());
    119. } catch (IOException exception) {
    120. }
    121. return false;
    122. }
    123. /**
    124. * 获取请求头里面的token
    125. */
    126. private String getRequestToken(HttpServletRequest httpRequest) {
    127. //从header中获取token
    128. String token = httpRequest.getHeader("token");
    129. //如果header中不存在token,则从参数中获取token
    130. if (StringUtils.isBlank(token)) {
    131. token = httpRequest.getParameter("token");
    132. }
    133. return token;
    134. }
    135. @Override
    136. public void doFilterInternal(ServletRequest request,
    137. ServletResponse response, FilterChain chain) throws ServletException, IOException {
    138. super.doFilterInternal(request, response, chain);
    139. }
    140. }

    10.创建ShiroConfig类

     

    1. package com.example.emos.wx.config.shiro;
    2. import org.apache.shiro.mgt.SecurityManager;
    3. import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    4. import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    5. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    6. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    7. import org.springframework.context.annotation.Bean;
    8. import org.springframework.context.annotation.Configuration;
    9. import javax.servlet.Filter;
    10. import java.util.HashMap;
    11. import java.util.LinkedHashMap;
    12. import java.util.Map;
    13. @Configuration
    14. public class ShiroConfig {
    15. @Bean("securityManager")
    16. public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
    17. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    18. securityManager.setRealm(oAuth2Realm);
    19. securityManager.setRememberMeManager(null);
    20. return securityManager;
    21. }
    22. @Bean("shiroFilter")
    23. public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,OAuth2Filter oAuth2Filter) {
    24. ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
    25. shiroFilter.setSecurityManager(securityManager);
    26. //oauth过滤
    27. Map filters = new HashMap<>();
    28. filters.put("oauth2", oAuth2Filter);
    29. shiroFilter.setFilters(filters);
    30. Map filterMap = new LinkedHashMap<>();
    31. filterMap.put("/webjars/**", "anon");
    32. filterMap.put("/druid/**", "anon");
    33. filterMap.put("/app/**", "anon");
    34. filterMap.put("/sys/login", "anon");
    35. filterMap.put("/swagger/**", "anon");
    36. filterMap.put("/v2/api-docs", "anon");
    37. filterMap.put("/swagger-ui.html", "anon");
    38. filterMap.put("/swagger-resources/**", "anon");
    39. filterMap.put("/captcha.jpg", "anon");
    40. filterMap.put("/user/register", "anon");
    41. filterMap.put("/user/login", "anon");
    42. filterMap.put("/test/**", "anon");
    43. filterMap.put("/**", "oauth2");
    44. shiroFilter.setFilterChainDefinitionMap(filterMap);
    45. return shiroFilter;
    46. }
    47. @Bean("lifecycleBeanPostProcessor")
    48. public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    49. return new LifecycleBeanPostProcessor();
    50. }
    51. @Bean
    52. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    53. AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    54. advisor.setSecurityManager(securityManager);
    55. return advisor;
    56. }
    57. }

    11.利用aop,把更新的令牌返回给客户端

     

    1. package com.example.emos.wx.aop;
    2. import com.example.emos.wx.common.util.R;
    3. import com.example.emos.wx.config.shiro.ThreadLocalToken;
    4. import org.aspectj.lang.ProceedingJoinPoint;
    5. import org.aspectj.lang.annotation.Around;
    6. import org.aspectj.lang.annotation.Aspect;
    7. import org.aspectj.lang.annotation.Pointcut;
    8. import org.springframework.beans.factory.annotation.Autowired;
    9. import org.springframework.stereotype.Component;
    10. @Aspect
    11. @Component
    12. public class TokenAspect {
    13. @Autowired
    14. private ThreadLocalToken threadLocalToken;
    15. @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..)))")
    16. public void aspect() {
    17. }
    18. @Around("aspect()")
    19. public Object around(ProceedingJoinPoint point) throws Throwable {
    20. R r = (R) point.proceed(); //方法执行结果
    21. String token = threadLocalToken.getToken();
    22. //如果ThreadLocal中存在Token,说明是更新的Token
    23. if (token != null) {
    24. r.put("token", token); //往响应中放置Token
    25. threadLocalToken.clear();
    26. }
    27. return r;
    28. }
    29. }

    12.返回给客户端的异常精简化,进行测试

    1. package com.example.emos.wx.config;
    2. import com.example.emos.wx.exception.EmosException;
    3. import lombok.extern.slf4j.Slf4j;
    4. import org.springframework.http.HttpStatus;
    5. import org.springframework.web.bind.MethodArgumentNotValidException;
    6. import org.springframework.web.bind.annotation.ExceptionHandler;
    7. import org.springframework.web.bind.annotation.ResponseBody;
    8. import org.springframework.web.bind.annotation.ResponseStatus;
    9. import org.springframework.web.bind.annotation.RestControllerAdvice;
    10. @Slf4j
    11. @RestControllerAdvice
    12. public class ExceptionAdvice {
    13. @ResponseBody
    14. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    15. @ExceptionHandler(Exception.class)
    16. public String validExceptionHandler(Exception e) {
    17. log.error("执行异常",e);
    18. if (e instanceof MethodArgumentNotValidException) {
    19. MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
    20. //将错误信息返回给前台
    21. return exception.getBindingResult().getFieldError().getDefaultMessage();
    22. }
    23. else if(e instanceof EmosException){
    24. EmosException exception=(EmosException)e;
    25. return exception.getMsg();
    26. }
    27. else if(e instanceof UnauthorizedException){
    28. return "你不具有相关权限";
    29. }
    30. else {
    31. return "后端执行异常";
    32. }
    33. }
    34. }

  • 相关阅读:
    7-10 LinkedHashSet
    FPGA按键消抖
    一个产品级MCU菜单框架设计
    光学设计相关知识概念(持续更新中)
    render() 函数即渲染函数 转换器 converter JS函数用于获取url参数:
    程序员个性终端指南(cmder、powershell、window terminal)
    数据库自动备份到gitee上,实现数据自动化备份
    分布式文件系统HDFS实践及原理详解part3
    CSS旋转、缩放、渐变
    IDM下载器使用教程
  • 原文地址:https://blog.csdn.net/qq_35207086/article/details/126657142