当前位置:   article > 正文

SpringBoot+Vue从零开始做网站7-整合shiro+jwt实现用户认证和授权_springboot集成shiro实现按钮权限 vue

springboot集成shiro实现按钮权限 vue

上一篇用shiro来登入存在用户认证的问题,而又不想用cookie session,所以决定使用jwt来做用户认证

Vue + sprintboot整合shiro+jwt实现用户认证和授权, 主要功能就是前端页面,需要登录的页面必须登陆后才可以访问,未登录的可以直接访问。所以主要还是登入登出功能,后端配置踩了不少坑,不过学习目的达成,有不对的地方再说吧~~哈哈

因为shiro的认证是根据sessionid来的,Shiro本身不提供维护用户、权限,而是通过Realm让开发人员自己注入到SecurityManager,从而让SecurityManager能得到合法的用户以及权限进行判断;

所以之前的代码都要改了,之前用shiro的登入但是认证的话和vue搭配起来总觉得麻烦。

最终决定还是用shiro+jwt来实现用户的授权和认证

JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。

利用一定的编码生成 Token,并在 Token 中加入一些非敏感信息,将其传递。 JWT是一种无状态处理用户身份验证的方法。基本上,每当创建token时,就可以永远使用它,或者直到它过期为止。 JWT生成器可以在生成的时候有一个指定过期时间的选项。

一个完整的 Token : eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

在本项目中,我们规定每次请求时,在请求头中带上 token ,通过 token 检验权限。首先设置哪些路由需要认证哪些不用,不用认证的路由直接放行,需要认证的则通过jwt过滤器进行认证操作,因为要过滤的都是限制访问的页面,所以如没有token,不放行并抛出异常,如果有token验证正常放行,token无效或者过期则拦截抛出异常。

认证方案(session 与 token)

最简单的认证方法,就是前端在每次请求时都加上用户名和密码,交由后端验证。这种方法的弊端有两个:

一,需要频繁查询数据库,导致服务器压力较大

二,安全性,如果信息被截取,攻击者就可以 一直 利用用户名密码登录(注意不是因为明文不安全,是由于无法控制时效性)

为了在某种程度上解决上述两个问题,有两种改进的方案 —— session 与 token。

session机制

session机制是一种服务器端的机制,Session可以用Cookie来实现,也可以用URL回写的机制来实现。用Cookie来实现的Session可以认为是对Cookie更高级的应用。一般使用cookie来实现session。

当客户端第一次访问服务器时,服务器创建一个session,同时生成一个唯一的会话key,即sessionID。接着sessionID及session分别作为key和value保存到缓存中,也可以保存到数据库中,然后服务器把sessionID以cookie的形式发送给客户端浏览器,浏览器下次访问服务器时直接携带上cookie中的sessionID,服务器再根据sessionID找到对应的session进行匹配。

session由服务端产生

以字典的形式存储,session保存状态信息,sessionid返回给客户端保存至本地

服务端需要一定的空间存储session,且一般为了提高响应速度,都是存储在内存中

sessionID会自动由浏览器带上

session 存储在内存中,在用户量较少时访问效率较高,但如果一个服务器保存了几十几百万个 session 就十分难顶了。同时由于同一用户的多次请求需要访问到同一服务器,不能简单做集群,需要通过一些策略(session sticky)来扩展,比较麻烦。

token就是令牌,比如你授权(登录)一个程序时,他就是个依据,判断你是否已经授权该软件;cookie就是写在客户端的一个txt文件,里面包括你登录信息之类的,这样你下次在登录某个网站,就会自动调用cookie自动登录用户名;

基于Token的身份验证是无状态的,我们不将用户信息存在服务器中。这种概念解决了在服务端存储信息时的许多问题。NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录,不用去担心扩展性的问题。

其实token与session的问题是一种时间与空间的博弈问题,session是空间换时间,而token是时间换空间。两者的选择要看具体情况而定。

token 和 session 本质功能相似,但如果跨站使用,token 会更方便一些。以下几点特性也会让你在程序中使用基于Token的身份验证:

无状态、可扩展

支持移动设备

跨程序调用

安全

token更多是对用户进行认证,然后对某一个应用进行授权。让某个应用拥有用户的部分信息。这个token仅供此应用使用。作为身份认证token安全性比session好

其他相关知识可以再去了解,然后就是代码了

首先引入依赖

<!--整合Shiro安全框架-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.8.0</version>
        </dependency>

<!--集成jwt实现token认证-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>

JWT工具类编写JwtUtils

我们利用 JWT 的工具类来生成我们的 token,这个工具类主要有生成 token 和 校验 token 两个方法

生成 token 时,指定 token 过期时间 EXPIRE_TIME 和签名密钥 SECRET,然后将 date 和 username 写入 token 中,并使用带有密钥的 HS256 签名算法进行签名

  1. package com.zjlovelt.shiro;
  2. import com.auth0.jwt.JWT;
  3. import com.auth0.jwt.JWTVerifier;
  4. import com.auth0.jwt.algorithms.Algorithm;
  5. import com.auth0.jwt.exceptions.JWTDecodeException;
  6. import com.auth0.jwt.interfaces.DecodedJWT;
  7. import com.zjlovelt.utils.Tools;
  8. import java.io.UnsupportedEncodingException;
  9. import java.util.Calendar;
  10. import java.util.Date;
  11. public class JwtUtils {
  12.     /**
  13.      * 密钥
  14.      * */
  15.     private static final String SECRET = "1008611";
  16.     //设置token有效时间 3天---为了方便测试先用1分钟试验
  17.     private static final long EXPIRE_TIME =  60 * 1000//3 * 24 * 60 * 60 * 1000;
  18.     public static String createToken(String username) throws UnsupportedEncodingException {
  19.         Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
  20.         //密文生成
  21.         String token = JWT.create()
  22.                 .withClaim("username", username)
  23.                 .withExpiresAt(date)
  24.                 .withIssuedAt(new Date())
  25.                 .sign(Algorithm.HMAC256(SECRET));
  26.         return token;
  27.     }
  28.     /**
  29.      * 验证token的有效性
  30.      * */
  31.     public static boolean verify(String token,String username) {
  32.         try {
  33.             JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).withClaim("username", username).build();
  34.             verifier.verify(token);
  35.             return true;
  36.         } catch (UnsupportedEncodingException e) {
  37.             return false;
  38.         }
  39.     }
  40.     /**
  41.      * 获取token列名
  42.      * **/
  43.     /**
  44.      * 通过载荷名字获取载荷的值
  45.      * */
  46.     public static String getClaim(String token, String name){
  47.         String claim = null;
  48.         try {
  49.             claim =  JWT.decode(token).getClaim(name).asString();
  50.         }catch (Exception e) {
  51.             return "getClaimFalse";
  52.         }
  53.         return claim;
  54.     }
  55.     //无需解密也可以获取token的信息
  56.     public static String getUsername(String token){
  57.         if (Tools.isEmpty(token)) {
  58.             return null;
  59.         }
  60.         try {
  61.             DecodedJWT jwt = JWT.decode(token);
  62.             return jwt.getClaim("username").asString();
  63.         } catch (JWTDecodeException e) {
  64.             return null;
  65.         }
  66.     }
  67. }

编写JwtToken类 继承 AuthenticationToken 

  1. package com.zjlovelt.shiro;
  2. import org.apache.shiro.authc.AuthenticationToken;
  3. public class JwtToken  implements AuthenticationToken {
  4.     private String token;
  5.     //构造方法
  6.     public JwtToken(String token) {
  7.         this.token = token;
  8.     }
  9.     @Override
  10.     public Object getPrincipal() {
  11.         return token;
  12.     }
  13.     @Override
  14.     public Object getCredentials() {
  15.         return token;
  16.     }
  17. }

编写Realm类

和之前一样,小改动  ,可以先看我的上一篇 shiro 的文章

  1. package com.zjlovelt.shiro;
  2. import com.zjlovelt.entity.SysUser;
  3. import com.zjlovelt.service.SysUserService;
  4. import com.zjlovelt.utils.Tools;
  5. import org.apache.shiro.authc.SimpleAuthenticationInfo;
  6. import org.apache.shiro.authc.*;
  7. import org.apache.shiro.authz.AuthorizationInfo;
  8. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  9. import org.apache.shiro.realm.AuthorizingRealm;
  10. import org.apache.shiro.subject.PrincipalCollection;
  11. import org.slf4j.Logger;
  12. import org.slf4j.LoggerFactory;
  13. import org.springframework.beans.factory.annotation.Autowired;
  14. public class ShiroRealm  extends AuthorizingRealm {
  15.     private Logger logger =  LoggerFactory.getLogger(this.getClass());
  16.     @Autowired
  17.     private SysUserService userService;
  18.     @Override
  19.     public boolean supports(AuthenticationToken token) {
  20.         return token instanceof JwtToken;
  21.     }
  22.     //重写获取授权信息方法  只有当检测用户需要权限或者需要判定角色的时候才会走
  23.     @Override
  24.     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
  25.         logger.info("doGetAuthorizationInfo+"+principalCollection.toString());
  26.         String userName = JwtUtils.getUsername(principalCollection.toString());
  27.         if (Tools.isEmpty(userName)) {
  28.             throw new AuthenticationException("token认证失败");
  29.         }
  30.         SysUser user = userService.getByUserName(userName);
  31.         //查询当前
  32.         SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  33.         if(user != null){
  34.             //赋予角色
  35.             /*List<Role> roles = roleService.selectRoleByUserId(user.getId());
  36.             for (Role role : roles) {
  37.                 info.addRole(role.getRoleKey());
  38.             }*/
  39.             //赋予权限
  40.             /*List<Menu> permissions = menuService.selectPermsByUserId(user.getId());
  41.             for (Menu permission : permissions) {
  42.                 info.addStringPermission(permission.getPerms());
  43.             }*/
  44.             //设置登录次数、时间
  45.             //userService.updateUserLogin(user);
  46.         }
  47.         return info;
  48.     }
  49.     // 获取认证信息:校验帐号和密码
  50.     //使用此方法进行用户名正确与否验证,
  51.     //     * 其实就是 过滤器传过来的token 然后进行 验证 authenticationToken.toString() 获取的就是
  52.     //     * 你的token字符串,然后你在里面做逻辑验证就好了,没通过的话直接抛出异常就可以了
  53.     @Override
  54.     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
  55.         logger.info("doGetAuthenticationInfo +"  + authenticationToken.toString());
  56.         String token = (String) authenticationToken.getCredentials();
  57.         String username = null;
  58.         //decode时候出错,可能是token的长度和规定好的不一样了
  59.         try {
  60.             username = JwtUtils.getUsername(token);
  61.         }catch (Exception e){
  62.             throw new AuthenticationException("token非法,不是规范的token,可能被篡改了,或者过期了");
  63.         }
  64.         SysUser user = userService.getByUserName(username);
  65.         if (user==null){
  66.             throw new AuthenticationException("该用户不存在");
  67.         }
  68.         if (!JwtUtils.verify(token, username) || username==null){
  69.             throw new AuthenticationException("token认证失效,token错误或者过期,重新登陆");
  70.         }
  71.         return new SimpleAuthenticationInfo(token, token, getName());
  72.     }
  73. }

.写JWTFiler(JWT过滤器)

在上一篇文章中,我们使用的是 shiro 默认的权限拦截 Filter,而因为 JWT 的整合,我们需要自定义自己的过滤器 JWTFilter,JWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原方法进行了重写。如果在 token 校验的过程中出现错误,如 token 校验失败或者过期,那么将该请求视为认证不通过,则重定向到 /noLogin/**

另外,我将跨域支持放到了该过滤器来处理

该过滤器主要有三步:

检验请求头是否带有 token ((HttpServletRequest) request).getHeader("Token") != null

如果带有 token,执行 shiro 的 login() 方法,将 token 提交到 Realm 中进行检验;如果没有 token,说明非法访问则拦截

  1. package com.zjlovelt.shiro;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import lombok.SneakyThrows;
  4. import org.apache.shiro.authc.AuthenticationException;
  5. import org.apache.shiro.authz.AuthorizationException;
  6. import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
  7. import org.slf4j.Logger;
  8. import org.slf4j.LoggerFactory;
  9. import org.springframework.http.HttpStatus;
  10. import org.springframework.web.bind.annotation.RequestMethod;
  11. import javax.servlet.ServletRequest;
  12. import javax.servlet.ServletResponse;
  13. import javax.servlet.http.HttpServletRequest;
  14. import javax.servlet.http.HttpServletResponse;
  15. import java.io.IOException;
  16. import java.net.URLEncoder;
  17. import java.util.LinkedHashMap;
  18. import java.util.Map;
  19. public class JwtFilter extends BasicHttpAuthenticationFilter {
  20.     private Logger logger =  LoggerFactory.getLogger(this.getClass());
  21.     private Map errorMap;
  22.     /**
  23.      * header中token标志
  24.      */
  25.     private static String TOKEN = "token";
  26.     /**
  27.      * 拦截器的前置  最先执行的 这里只做了一个跨域设置
  28.      */
  29.     @Override
  30.     protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
  31.         System.out.println("JwtFilter -----> preHandle() 方法执行");
  32.         HttpServletRequest req = (HttpServletRequest) request;
  33.         HttpServletResponse res = (HttpServletResponse) response;
  34.         res.setHeader("Access-control-Allow-Origin", req.getHeader("origin"));
  35.         res.setHeader("Access-control-Allow-Methods""GET,POST,OPTIONS,PUT,DELETE");
  36.         res.setHeader("Access-Control-Allow-Credentials""true");
  37.         //res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
  38.         // 允许客户端,发一个新的请求头jwt
  39.         res.setHeader("Access-Control-Allow-Headers""Origin,X-Requested-With, Content-Type, Accept, token");
  40.         // 允许客户端,处理一个新的响应头jwt
  41.         res.setHeader("Access-Control-Expose-Headers""token");
  42.         // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
  43.         if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
  44.             res.setStatus(HttpStatus.OK.value());
  45.             return false;
  46.         }
  47.         return super.preHandle(request, response);
  48.     }
  49.         /*
  50.          *  preHandle 执行完之后会执行这个方法
  51.          * 再这个方法中 我们根据条件判断去去执行isLoginAttempt和executeLogin方法
  52.          * 1. 返回true,shiro就直接允许访问url
  53.          * */
  54.         @SneakyThrows
  55.         @Override
  56.         protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) {
  57.             logger.info("JwtFilter -----> isAccessAllowed() 方法执行");
  58.             /**
  59.              * 先去调用 isLoginAttempt方法 字面意思就是是否尝试登陆 如果为true
  60.              * 执行executeLogin方法
  61.              */
  62.             if (isLoginAttempt(request, response)) {
  63.                 try {
  64.                     executeLogin(request, response);
  65.                     return true;
  66.                 } catch (Exception e) {
  67.                     //token 错误
  68.                     tokenError(response, e.getMessage());
  69.                     return false;
  70.                 }
  71.             } else {
  72.                 tokenError(response, "token not in");
  73.                 return false;  如果请求头不存在 Token,直接返回错误信息
  74.             }
  75.         }
  76.         /**
  77.          * 这里我们只是简单去做一个判断请求头中的token信息是否为空
  78.          * 如果没有我们想要的请求头信息则直接返回false
  79.          * */
  80.         @Override
  81.         protected boolean isLoginAttempt(ServletRequest request, ServletResponse response){
  82.             logger.info("JwtFilter -----> isLoginAttempt() 方法执行");
  83.             HttpServletRequest req = (HttpServletRequest) request;
  84.             //判断是否是登录请求
  85.             String token = req.getHeader("token");
  86.             return token != null;
  87.         }
  88.         /**
  89.          * 执行登陆
  90.          * 因为已经判断token不为空了,所以直接执行登陆逻辑
  91.          * token放入JwtToken类中去
  92.          * 然后getSubject方法是调用到了ShiroRealm的 执行方法  因为上面我是抛错的所有最后做个异常捕获就好了
  93.          * */
  94.         @Override
  95.         protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
  96.             logger.info("JwtFilter -----> executeLogin() 方法执行");
  97.             HttpServletRequest req = (HttpServletRequest) request;
  98.             String header = req.getHeader(TOKEN);
  99.             JwtToken token = new JwtToken(header);
  100.             //然后交给自定义的realm对象去登陆, 如果错误他会抛出异常并且捕获
  101.             logger.info("-----执行登陆开始-----");
  102.            try {
  103.                 getSubject(request, response).login(token);
  104.            } catch (AuthenticationException  e) {
  105.                 e.printStackTrace();
  106.                 tokenError(response, "token auth not success");
  107.                 return false;
  108.            }
  109.             logger.info("-----执行登陆结束----- 未抛出异常");
  110.             return true;
  111.         }
  112.     /**
  113.      *  isAccessAllowed()返回false便会执行这个方法,
  114.      * @param request
  115.      * @param response
  116.      * @return 返回false,则过滤器的流程结束且不会执行访问controller的方法
  117.      * @throws Exception
  118.      */
  119.     @Override
  120.     public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  121.         return false;
  122.     }
  123.     /**
  124.      * token问题响应
  125.      *
  126.      * @param response
  127.      * @param msg
  128.      * @return void
  129.      * @author: zhihao
  130.      * @date: 2019/12/24
  131.      * {@link #}
  132.      */
  133.     private void tokenError(ServletResponse response,String msg) throws IOException {
  134.         /*errorMap = new LinkedHashMap();
  135.         errorMap.put("success", "false");
  136.         errorMap.put("msg", msg);
  137.         //响应token为空
  138.         response.setContentType("application/json;charset=UTF-8");
  139.         response.setCharacterEncoding("UTF-8");
  140.         response.resetBuffer(); //清空第一次流响应的内容
  141.         //转成json格式
  142.         ObjectMapper object = new ObjectMapper();
  143.         String asString = object.writeValueAsString(errorMap);
  144.         response.getWriter().println(asString);*/
  145.         try {
  146.             HttpServletResponse httpServletResponse = (HttpServletResponse) response;
  147.             //设置编码,否则中文字符在重定向时会变为空字符串
  148.             msg = URLEncoder.encode(msg, "UTF-8");
  149.             httpServletResponse.sendRedirect("/noLogin?message=" + msg);
  150.         } catch (IOException e) {
  151.             logger.error(e.getMessage());
  152.         }
  153.     }
  154. }

配置ShiroConfig将配置注入到容器中

设置好我们自定义的 filter,并使所有请求通过我们的过滤器,除了我们不需要认证的

配置package com.zjlovelt.shiro;

  1. import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
  2. import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
  3. import org.apache.shiro.mgt.DefaultSubjectDAO;
  4. import org.apache.shiro.spring.LifecycleBeanPostProcessor;
  5. import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
  6. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
  7. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
  8. import org.apache.shiro.mgt.SecurityManager;
  9. import org.springframework.context.annotation.Bean;
  10. import org.springframework.context.annotation.Configuration;
  11. import org.springframework.context.annotation.DependsOn;
  12. import java.util.LinkedHashMap;
  13. import java.util.Map;
  14. import javax.servlet.Filter;
  15. /**
  16.  * shiro配置类
  17.  * Created by zj on 2022/4/19.
  18.  */
  19. @Configuration
  20. public class ShiroConfiguration {
  21.     /**
  22.      * LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
  23.      * 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
  24.      * 主要是AuthorizingRealm类的子类,以及EhCacheManager类。
  25.      */
  26.     @Bean(name = "lifecycleBeanPostProcessor")
  27.     public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
  28.         return new LifecycleBeanPostProcessor();
  29.     }
  30. /*
  31.     */
  32. /**
  33.      * HashedCredentialsMatcher,这个类是为了对密码进行编码的,
  34.      * 防止密码在数据库里明码保存,当然在登陆认证的时候,
  35.      * 这个类也负责对form里输入的密码进行编码。
  36.      *//*
  37.     @Bean(name = "hashedCredentialsMatcher")
  38.     public HashedCredentialsMatcher hashedCredentialsMatcher() {
  39.         HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
  40.         credentialsMatcher.setHashAlgorithmName("MD5");
  41.         credentialsMatcher.setHashIterations(2);
  42.         credentialsMatcher.setStoredCredentialsHexEncoded(true);
  43.         return credentialsMatcher;
  44.     }
  45. */
  46.     /**
  47.      * ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,
  48.      * 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
  49.      */
  50.     @Bean(name = "shiroRealm")
  51.     @DependsOn("lifecycleBeanPostProcessor")
  52.     public ShiroRealm shiroRealm() {
  53.         ShiroRealm realm = new ShiroRealm();
  54.        // realm.setCredentialsMatcher(hashedCredentialsMatcher());
  55.         return realm;
  56.     }
  57.     /**
  58.      * EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来,
  59.      * 然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。
  60.      */
  61. /*    @Bean(name = "ehCacheManager")
  62.     @DependsOn("lifecycleBeanPostProcessor")
  63.     public EhCacheManager ehCacheManager() {
  64.         return new EhCacheManager();
  65.     }*/
  66.     /**
  67.      * SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
  68.      * //
  69.      */
  70.     @Bean(name = "securityManager")
  71.     public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
  72.         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  73. //        securityManager.setCacheManager(ehCacheManager());
  74.         //关闭自带session
  75.         DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
  76.         DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
  77.         sessionStorageEvaluator.setSessionStorageEnabled(false);
  78.         subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
  79.         securityManager.setSubjectDAO(subjectDAO);
  80.         securityManager.setRealm(shiroRealm);
  81.         return securityManager;
  82.     }
  83.     /**
  84.      * ShiroFilter是整个Shiro的入口点,用于拦截需要安全控制的请求进行处理
  85.      * ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
  86.      * 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
  87.      */
  88.     @Bean
  89.     public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
  90.         ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  91.         //Shiro的核心安全接口,这个属性是必须的
  92.         shiroFilterFactoryBean.setSecurityManager(securityManager);
  93.         //添加自己的过滤器 并且取名为filter
  94.         Map<String, Filter> filterMap = new LinkedHashMap<>();
  95.         //设置自定义的JWT过滤器
  96.         filterMap.put("jwt",  new JwtFilter());
  97.         shiroFilterFactoryBean.setFilters(filterMap);
  98.         //设置无权限跳转的url 权限验证如果没权限跳转---此处拦截规则为拦截所有后台管理系统接口api。。。其他通通放行
  99.         Map<String, String> filterChainDefinitionManager = new LinkedHashMap<String, String>();
  100.         filterChainDefinitionManager.put("/api/**""jwt");
  101.         shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
  102.         shiroFilterFactoryBean.setLoginUrl("/login");
  103.         return shiroFilterFactoryBean;
  104.     }
  105.     /**
  106.      * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
  107.      */
  108.    /* @Bean
  109.     @ConditionalOnMissingBean
  110.     public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
  111.         DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
  112.         defaultAAP.setProxyTargetClass(true);
  113.         return defaultAAP;
  114.     }*/
  115.     /**
  116.      * AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
  117.      * 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
  118.      */
  119.     @Bean
  120.     public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
  121.         AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
  122.         aASA.setSecurityManager(securityManager);
  123.         return aASA;
  124.     }
  125. }

权限校验或者角色校验

坑留意:

1、reaml 中 校验 token一直有问题,报错  Odd number of characters. 

这个问题是因为上一篇文章使用了shiro的登入校验,改成jwt没有将ShiroConfiguration配置的hashedCredentialsMatcher去掉,导致即使最后一直报错。

解决方法就是把将ShiroConfiguration配置的hashedCredentialsMatcher去掉

  1. /**
  2.      * HashedCredentialsMatcher,这个类是为了对密码进行编码的,
  3.      * 防止密码在数据库里明码保存,当然在登陆认证的时候,
  4.      * 这个类也负责对form里输入的密码进行编码。
  5.      *//*
  6.     @Bean(name = "hashedCredentialsMatcher")
  7.     public HashedCredentialsMatcher hashedCredentialsMatcher() {
  8.         HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
  9.         credentialsMatcher.setHashAlgorithmName("MD5");
  10.         credentialsMatcher.setHashIterations(2);
  11.         credentialsMatcher.setStoredCredentialsHexEncoded(true);
  12.         return credentialsMatcher;
  13.     }
  14. */

删掉之后就可以这样写 return new SimpleAuthenticationInfo(token, token, getName());

2、前端请求跨域

之前处理过跨域问题,但是这次是jwt验证的时候出现的跨域,解决方式就是在JwtFilter中的preHandle做跨域设置,设置好后有各种跨域问题,根据前端具体报错一步一步解决。

一些注意事项:

当跨域请求需要携带cookie时,就是前端的request.js的  withCredentials: true时,请求头中需要设置Access-Control-Allow-Credentials:true。

Access-Control-Allow-Credentials值为true时,Access-Control-Allow-Origin必须有明确的值,不能是通配符(*)

然后就是jwt验证得加上

 res.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, token");

res.setHeader("Access-Control-Expose-Headers", "token");

完整代码:

  1. /**
  2.      * 拦截器的前置  最先执行的 这里只做了一个跨域设置
  3.      */
  4.     @Override
  5.     protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
  6.         System.out.println("JwtFilter -----> preHandle() 方法执行");
  7.         HttpServletRequest req = (HttpServletRequest) request;
  8.         HttpServletResponse res = (HttpServletResponse) response;
  9.         res.setHeader("Access-control-Allow-Origin", req.getHeader("origin"));   
  10.         res.setHeader("Access-control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
  11.         res.setHeader("Access-Control-Allow-Credentials", "true");
  12.         //res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
  13.         // 允许客户端,发一个新的请求头jwt
  14.         res.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, token");
  15.         // 允许客户端,处理一个新的响应头jwt
  16.         res.setHeader("Access-Control-Expose-Headers", "token");
  17.         // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
  18.         if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
  19.             res.setStatus(HttpStatus.OK.value());
  20.             return false;
  21.         }
  22.         return super.preHandle(request, response);
  23.     }

3、前端请求弹出登录框,总的来说就是JWT用户认证失败时怎么处理的,前端vue当token在后台验证的时候如果不通过,前端不是提示对应错误码的提示信息,而是统一报500的内部错误。

  1.  try {
  2.                     executeLogin(request, response);
  3.                     return true;
  4.                 } catch (Exception e) {
  5.                     //token 错误
  6.                     tokenError(response, e.getMessage());
  7.                     return false;
  8.                 }

直接抛出异常肯定不行,前端没法搞,前端需要根据后端返回值判断是不是需要跳到登录页。

然后就是试了在异常的时候重新返回响应结果,但是还是有问题,可能是没写好

  1.   private void tokenError(ServletResponse response,String msg) throws IOException {
  2.         errorMap = new LinkedHashMap();
  3.         errorMap.put("success", "false");
  4.         errorMap.put("msg", msg);
  5.         //响应token为空
  6.         response.setContentType("application/json;charset=UTF-8");
  7.         response.setCharacterEncoding("UTF-8");
  8.         response.resetBuffer(); //清空第一次流响应的内容
  9.         //转成json格式
  10.         ObjectMapper object = new ObjectMapper();
  11.         String asString = object.writeValueAsString(errorMap);
  12.         response.getWriter().println(asString);
  13.     }

最后还是用了重定向的方式。。。最好也有用,那就先这么用着吧,等以后再改

  1.   private void tokenError(ServletResponse response,String msg) throws IOException {
  2.         try {
  3.             HttpServletResponse httpServletResponse = (HttpServletResponse) response;
  4.             //设置编码,否则中文字符在重定向时会变为空字符串
  5.             msg = URLEncoder.encode(msg, "UTF-8");
  6.             httpServletResponse.sendRedirect("/noLogin?message=" + msg);
  7.         } catch (IOException e) {
  8.             logger.error(e.getMessage());
  9.         }
  10.     }

后端讲完了,然后就是前端了。

前端存储方案 (cookie、localStorage、sessionStorage)

还是选择localStorage,但是在上一篇的基础上做了修改,登入登出方法也没有改,和上篇一样,主要是改了路由守卫拦截方法和前端请求方法。

request.js修改,为每次请求加上token,

  1. /**
  2.  * 请求拦截
  3.  */
  4. service.interceptors.request.use(
  5.     config => {
  6.         let token = localStorage.getItem('ms_token');
  7.         // 为请求头添加token字段为服务端返回的token
  8.         config.headers['token'] = token
  9.         return config;
  10.     },
  11.     error => {
  12.         console.log(error);
  13.         return Promise.reject();
  14.     }
  15. );

router/index.js修改路由守卫

  1. router.beforeEach((to, from, next) => {
  2.     document.title = `${to.meta.title} | ltBlog`;
  3.     const token = localStorage.getItem('ms_token');
  4.     let currentRouteType = fnCurrentRouteType(to, globalRoutes)
  5.     if (currentRouteType !== 'global') {
  6.         currentRouteType = fnCurrentRouteType(to, skipLoadMenusRoutes)
  7.     }
  8.     //请求的路由在【不用登陆也能访问路由数组】中,则不用跳转到登录页
  9.     if (currentRouteType === 'global') {
  10.         next();
  11.     } else {
  12.         //如果路由为空,并且不在【不用登陆也能访问路由数组】中 则跳转到登录页
  13.         if(!token){
  14.             next('/login');
  15.         }else{
  16.             //每次跳转路由都请求后端校验token是否有效
  17.             authtoken().then((res) => {
  18.                 console.log(res)
  19.                 //如果token无效或者已过期 则跳转到登录页并清除localStorage存储的token
  20.                 if(res.success === false){
  21.                     localStorage.removeItem("ms_token");
  22.                     ElMessage.error("登录过期,请重新登录");
  23.                     next('/login');
  24.                 }else{
  25.                     next();
  26.                 }
  27.             });
  28.         }
  29.     }
  30. });

关于登出,目前是只是设置了token的有效期,在有效期内用户可以一直保持登录状态,重新登录会生成新的token,退出登录就删掉前端存的token让用户区去重新登陆即可。

实际开发中遇到了问题再解决吧,1总能解决掉的,踩了很多坑现在还有点忘了  所以没记录。。。 

接下来的开发后端就简单了,无非增删改查,主要是前端了,明天继续搞起~

在博客中查看:从零开始做网站7-整合shiro+jwt实现用户认证和授权 - ZJBLOG

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/53914
推荐阅读
相关标签
  

闽ICP备14008679号