生活不易、且行且珍惜。网站首页 程序人生
从零开始做网站7-整合shiro+jwt实现用户认证和授权
发布时间:2022-05-25 19:18编辑:zj 阅读:文章分类:
网站互动QQ群:170915747
上一篇用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 签名算法进行签名
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
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 的文章
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,说明非法访问则拦截
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;
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");
完整代码:
/** * 拦截器的前置 最先执行的 这里只做了一个跨域设置 */ @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;
}
直接抛出异常肯定不行,前端没法搞,前端需要根据后端返回值判断是不是需要跳到登录页。
然后就是试了在异常的时候重新返回响应结果,但是还是有问题,可能是没写好
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); }
最后还是用了重定向的方式。。。最好也有用,那就先这么用着吧,等以后再改
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,
/** * 请求拦截 */ 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修改路由守卫
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总能解决掉的,踩了很多坑现在还有点忘了 所以没记录。。。
接下来的开发后端就简单了,无非增删改查,主要是前端了,明天继续搞起~
#去评论一下
标签:#Springboot#Vue
版权声明:本博客的所有原创内容皆为作品作者所有
转载请注明:来自ZJBLOG 链接:www.zjhuiwan.cn


「万物皆有时,比如你我相遇」
感谢大佬打赏【请选择支付宝或微信,再选择金额】
使用微信扫描二维码完成支付
