上一篇用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总能解决掉的,踩了很多坑现在还有点忘了 所以没记录。。。
接下来的开发后端就简单了,无非增删改查,主要是前端了,明天继续搞起~