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

2022-06-21 08:57:20 浏览数 (1)

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

}

权限校验或者角色校验

坑留意:

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、前端请求跨域

之前处理过跨域问题,但是这次是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的内部错误。

 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());
        }
    }			

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

前端存储方案 (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让用户区去重新登陆即可。

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

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

0 人点赞