前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零开始做网站7-整合shiro+jwt实现用户认证和授权

从零开始做网站7-整合shiro+jwt实现用户认证和授权

作者头像
sunonzj
发布2022-06-21 08:57:20
1K0
发布2022-06-21 08:57:20
举报
文章被收录于专栏:zjblogzjblog

上一篇用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好

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

首先引入依赖

代码语言:javascript
复制
<!--整合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 签名算法进行签名

代码语言:javascript
复制
package com.zjlovelt.shiro;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zjlovelt.utils.Tools;

import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;

public class JwtUtils {

    /**
     * 密钥
     * */
    private static final String SECRET = "1008611";

    //设置token有效时间 3天---为了方便测试先用1分钟试验
    private static final long EXPIRE_TIME =  60 * 1000; //3 * 24 * 60 * 60 * 1000;


    public static String createToken(String username) throws UnsupportedEncodingException {
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        //密文生成
        String token = JWT.create()
                .withClaim("username", username)
                .withExpiresAt(date)
                .withIssuedAt(new Date())
                .sign(Algorithm.HMAC256(SECRET));
        return token;
    }

    /**
     * 验证token的有效性
     * */
    public static boolean verify(String token,String username) {
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).withClaim("username", username).build();
            verifier.verify(token);
            return true;
        } catch (UnsupportedEncodingException e) {
            return false;
        }
    }

    /**
     * 获取token列名
     * **/
    /**
     * 通过载荷名字获取载荷的值
     * */

    public static String getClaim(String token, String name){
        String claim = null;
        try {
            claim =  JWT.decode(token).getClaim(name).asString();
        }catch (Exception e) {
            return "getClaimFalse";
        }
        return claim;
    }

    //无需解密也可以获取token的信息
    public static String getUsername(String token){
        if (Tools.isEmpty(token)) {
            return null;
        }
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }

    }
}

编写JwtToken类 继承 AuthenticationToken 

代码语言:javascript
复制
package com.zjlovelt.shiro;

import org.apache.shiro.authc.AuthenticationToken;

public class JwtToken  implements AuthenticationToken {
    private String token;

    //构造方法
    public JwtToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

编写Realm类

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

代码语言:javascript
复制
package com.zjlovelt.shiro;

import com.zjlovelt.entity.SysUser;
import com.zjlovelt.service.SysUserService;
import com.zjlovelt.utils.Tools;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

public class ShiroRealm  extends AuthorizingRealm {

    private Logger logger =  LoggerFactory.getLogger(this.getClass());

    @Autowired
    private SysUserService userService;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }


    //重写获取授权信息方法  只有当检测用户需要权限或者需要判定角色的时候才会走
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("doGetAuthorizationInfo+"+principalCollection.toString());
        String userName = JwtUtils.getUsername(principalCollection.toString());
        if (Tools.isEmpty(userName)) {
            throw new AuthenticationException("token认证失败");
        }
        SysUser user = userService.getByUserName(userName);
        //查询当前
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if(user != null){
            //赋予角色
            /*List<Role> roles = roleService.selectRoleByUserId(user.getId());
            for (Role role : roles) {
                info.addRole(role.getRoleKey());
            }*/
            //赋予权限
            /*List<Menu> permissions = menuService.selectPermsByUserId(user.getId());
            for (Menu permission : permissions) {
                info.addStringPermission(permission.getPerms());
            }*/

            //设置登录次数、时间
            //userService.updateUserLogin(user);
        }

        return info;
    }


    // 获取认证信息:校验帐号和密码
    //使用此方法进行用户名正确与否验证,
    //     * 其实就是 过滤器传过来的token 然后进行 验证 authenticationToken.toString() 获取的就是
    //     * 你的token字符串,然后你在里面做逻辑验证就好了,没通过的话直接抛出异常就可以了
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        logger.info("doGetAuthenticationInfo +"  + authenticationToken.toString());

        String token = (String) authenticationToken.getCredentials();
        String username = null;
        //decode时候出错,可能是token的长度和规定好的不一样了
        try {
            username = JwtUtils.getUsername(token);
        }catch (Exception e){
            throw new AuthenticationException("token非法,不是规范的token,可能被篡改了,或者过期了");
        }

        SysUser user = userService.getByUserName(username);

        if (user==null){
            throw new AuthenticationException("该用户不存在");
        }
        if (!JwtUtils.verify(token, username) || username==null){
            throw new AuthenticationException("token认证失效,token错误或者过期,重新登陆");
        }

        return new SimpleAuthenticationInfo(token, token, getName());
    }

}

.写JWTFiler(JWT过滤器)

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

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

该过滤器主要有三步:

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

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

代码语言:javascript
复制
package com.zjlovelt.shiro;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.Map;

public class JwtFilter extends BasicHttpAuthenticationFilter {

    private Logger logger =  LoggerFactory.getLogger(this.getClass());
    private Map errorMap;

    /**
     * header中token标志
     */
    private static String TOKEN = "token";

    /**
     * 拦截器的前置  最先执行的 这里只做了一个跨域设置
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("JwtFilter -----> preHandle() 方法执行");
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Access-control-Allow-Origin", req.getHeader("origin"));
        res.setHeader("Access-control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        res.setHeader("Access-Control-Allow-Credentials", "true");
        //res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
        // 允许客户端,发一个新的请求头jwt
        res.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, token");
        // 允许客户端,处理一个新的响应头jwt
        res.setHeader("Access-Control-Expose-Headers", "token");
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态

        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            res.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

        /*
         *  preHandle 执行完之后会执行这个方法
         * 再这个方法中 我们根据条件判断去去执行isLoginAttempt和executeLogin方法
         * 1. 返回true,shiro就直接允许访问url
         * */
        @SneakyThrows
        @Override
        protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) {
            logger.info("JwtFilter -----> isAccessAllowed() 方法执行");
            /**
             * 先去调用 isLoginAttempt方法 字面意思就是是否尝试登陆 如果为true
             * 执行executeLogin方法
             */
            if (isLoginAttempt(request, response)) {
                try {
                    executeLogin(request, response);
                    return true;
                } catch (Exception e) {
                    //token 错误
                    tokenError(response, e.getMessage());
                    return false;
                }
            } else {
                tokenError(response, "token not in");
                return false;  ////如果请求头不存在 Token,直接返回错误信息
            }

        }




        /**
         * 这里我们只是简单去做一个判断请求头中的token信息是否为空
         * 如果没有我们想要的请求头信息则直接返回false
         * */
        @Override
        protected boolean isLoginAttempt(ServletRequest request, ServletResponse response){
            logger.info("JwtFilter -----> isLoginAttempt() 方法执行");
            HttpServletRequest req = (HttpServletRequest) request;
            //判断是否是登录请求
            String token = req.getHeader("token");
            return token != null;
        }


        /**
         * 执行登陆
         * 因为已经判断token不为空了,所以直接执行登陆逻辑
         * token放入JwtToken类中去
         * 然后getSubject方法是调用到了ShiroRealm的 执行方法  因为上面我是抛错的所有最后做个异常捕获就好了
         * */
        @Override
        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws IOException {
            logger.info("JwtFilter -----> executeLogin() 方法执行");
            HttpServletRequest req = (HttpServletRequest) request;
            String header = req.getHeader(TOKEN);
            JwtToken token = new JwtToken(header);
            //然后交给自定义的realm对象去登陆, 如果错误他会抛出异常并且捕获
            logger.info("-----执行登陆开始-----");
           try {
                getSubject(request, response).login(token);
           } catch (AuthenticationException  e) {
                e.printStackTrace();
                tokenError(response, "token auth not success");
                return false;
           }
            logger.info("-----执行登陆结束----- 未抛出异常");
            return true;
        }

    /**
     *  isAccessAllowed()返回false便会执行这个方法,
     * @param request
     * @param response
     * @return 返回false,则过滤器的流程结束且不会执行访问controller的方法
     * @throws Exception
     */
    @Override
    public boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        return false;
    }


    /**
     * token问题响应
     *
     * @param response
     * @param msg
     * @return void
     * @author: zhihao
     * @date: 2019/12/24
     * {@link #}
     */
    private void tokenError(ServletResponse response,String msg) throws IOException {
        /*errorMap = new LinkedHashMap();
        errorMap.put("success", "false");
        errorMap.put("msg", msg);
        //响应token为空
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.resetBuffer(); //清空第一次流响应的内容
        //转成json格式
        ObjectMapper object = new ObjectMapper();
        String asString = object.writeValueAsString(errorMap);
        response.getWriter().println(asString);*/

        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            //设置编码,否则中文字符在重定向时会变为空字符串
            msg = URLEncoder.encode(msg, "UTF-8");
            httpServletResponse.sendRedirect("/noLogin?message=" + msg);
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }

}

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

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

配置package com.zjlovelt.shiro;

代码语言:javascript
复制
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;

/**
 * shiro配置类
 * Created by zj on 2022/4/19.
 */
@Configuration
public class ShiroConfiguration {

    /**
     * LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
     * 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
     * 主要是AuthorizingRealm类的子类,以及EhCacheManager类。
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

/*
    */
/**
     * HashedCredentialsMatcher,这个类是为了对密码进行编码的,
     * 防止密码在数据库里明码保存,当然在登陆认证的时候,
     * 这个类也负责对form里输入的密码进行编码。
     *//*

    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(2);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }
*/

    /**
     * ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,
     * 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
     */
    @Bean(name = "shiroRealm")
    @DependsOn("lifecycleBeanPostProcessor")
    public ShiroRealm shiroRealm() {
        ShiroRealm realm = new ShiroRealm();
       // realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    /**
     * EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来,
     * 然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。
     */
/*    @Bean(name = "ehCacheManager")
    @DependsOn("lifecycleBeanPostProcessor")
    public EhCacheManager ehCacheManager() {
        return new EhCacheManager();
    }*/

    /**
     * SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
     * //
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//        securityManager.setCacheManager(ehCacheManager());
        //关闭自带session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        securityManager.setRealm(shiroRealm);
        return securityManager;
    }

    /**
     * ShiroFilter是整个Shiro的入口点,用于拦截需要安全控制的请求进行处理
     * ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
     * 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //添加自己的过滤器 并且取名为filter
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        //设置自定义的JWT过滤器
        filterMap.put("jwt",  new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        //设置无权限跳转的url 权限验证如果没权限跳转---此处拦截规则为拦截所有后台管理系统接口api。。。其他通通放行
        Map<String, String> filterChainDefinitionManager = new LinkedHashMap<String, String>();
        filterChainDefinitionManager.put("/api/**", "jwt");
	
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
        shiroFilterFactoryBean.setLoginUrl("/login");
        return shiroFilterFactoryBean;
    }

    /**
     * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
     */
   /* @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }*/

    /**
     * AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
     * 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
        aASA.setSecurityManager(securityManager);
        return aASA;
    }

}

权限校验或者角色校验

Snipaste_2022-04-23_11-50-42.png
Snipaste_2022-04-23_11-50-42.png

坑留意:

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

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

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

/**

     * HashedCredentialsMatcher,这个类是为了对密码进行编码的,

     * 防止密码在数据库里明码保存,当然在登陆认证的时候,

     * 这个类也负责对form里输入的密码进行编码。

     *//*

    @Bean(name = "hashedCredentialsMatcher")

    public HashedCredentialsMatcher hashedCredentialsMatcher() {

        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();

        credentialsMatcher.setHashAlgorithmName("MD5");

        credentialsMatcher.setHashIterations(2);

        credentialsMatcher.setStoredCredentialsHexEncoded(true);

        return credentialsMatcher;

    }

*/

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

2、前端请求跨域

QQ截图20220421175307.png
QQ截图20220421175307.png

之前处理过跨域问题,但是这次是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");

完整代码:

代码语言:javascript
复制
	/**
     * 拦截器的前置  最先执行的 这里只做了一个跨域设置
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        System.out.println("JwtFilter -----> preHandle() 方法执行");
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        res.setHeader("Access-control-Allow-Origin", req.getHeader("origin"));   
        res.setHeader("Access-control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        res.setHeader("Access-Control-Allow-Credentials", "true");
        //res.setHeader("Access-control-Allow-Headers", req.getHeader("Access-Control-Request-Headers"));
        // 允许客户端,发一个新的请求头jwt
        res.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With, Content-Type, Accept, token");
        // 允许客户端,处理一个新的响应头jwt
        res.setHeader("Access-Control-Expose-Headers", "token");
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态

        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            res.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

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

QQ截图20220421175355.png
QQ截图20220421175355.png

 try {

                    executeLogin(request, response);

                    return true;

                } catch (Exception e) {

                    //token 错误

                    tokenError(response, e.getMessage());

                    return false;

                }

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

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

代码语言:javascript
复制
  private void tokenError(ServletResponse response,String msg) throws IOException {
        errorMap = new LinkedHashMap();
        errorMap.put("success", "false");
        errorMap.put("msg", msg);
        //响应token为空
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.resetBuffer(); //清空第一次流响应的内容
        //转成json格式
        ObjectMapper object = new ObjectMapper();
        String asString = object.writeValueAsString(errorMap);
        response.getWriter().println(asString);
    }

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

代码语言:javascript
复制
  private void tokenError(ServletResponse response,String msg) throws IOException {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            //设置编码,否则中文字符在重定向时会变为空字符串
            msg = URLEncoder.encode(msg, "UTF-8");
            httpServletResponse.sendRedirect("/noLogin?message=" + msg);
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }			

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

Snipaste_2022-04-23_11-08-36.png
Snipaste_2022-04-23_11-08-36.png

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

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

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

代码语言:javascript
复制
/**
 * 请求拦截
 */
service.interceptors.request.use(
    config => {
        let token = localStorage.getItem('ms_token');
        // 为请求头添加token字段为服务端返回的token
        config.headers['token'] = token
        return config;
    },
    error => {
        console.log(error);
        return Promise.reject();
    }
);

router/index.js修改路由守卫

代码语言:javascript
复制
router.beforeEach((to, from, next) => {
    document.title = `${to.meta.title} | ltBlog`;
    const token = localStorage.getItem('ms_token');

    let currentRouteType = fnCurrentRouteType(to, globalRoutes)
    if (currentRouteType !== 'global') {
        currentRouteType = fnCurrentRouteType(to, skipLoadMenusRoutes)
    }
    //请求的路由在【不用登陆也能访问路由数组】中,则不用跳转到登录页
    if (currentRouteType === 'global') {
        next();
    } else {
        //如果路由为空,并且不在【不用登陆也能访问路由数组】中 则跳转到登录页
        if(!token){
            next('/login');
        }else{
            //每次跳转路由都请求后端校验token是否有效
            authtoken().then((res) => {
                console.log(res)
                //如果token无效或者已过期 则跳转到登录页并清除localStorage存储的token
                if(res.success === false){
                    localStorage.removeItem("ms_token");
                    ElMessage.error("登录过期,请重新登录");
                    next('/login');
                }else{
                    next();
                }
            });
        }
    }
});

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

Snipaste_2022-04-23_11-07-31.png
Snipaste_2022-04-23_11-07-31.png

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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-05-25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云顾问
云顾问(Tencent Cloud Smart Advisor)是一款提供可视化云架构IDE和多个ITOM领域垂直应用的云上治理平台,以“一个平台,多个应用”为产品理念,依托腾讯云海量运维专家经验,助您打造卓越架构,实现便捷、灵活的一站式云上治理。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档