文章目录
- 前言
- 一、jwt后端配置
- 1.导入依赖
- 2.jwt方法类
- 3.token生成
- 4.jwt拦截器
- 5.jwt拦截器配置
- 二、jwt前端配置
- 1.获取token
- 2.封装axios
- 三、踩坑记录
- 1.静态资源被拦截(一、5)
- 2./error被拦截(一、5)
- 3.yml项目路径(一、5)
前言
通过jwt实现单点登录,后端项目基于spring boot实现,前端使用vue.js示范。
jwt是什么?原理是什么?诸如此类的问题这里不提,本文记录jwt登录验证的前后端实现过程及踩的坑
一、jwt后端配置
1.导入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
2.jwt方法类
包含token相关参数,生成token、验证token、解析token方法
package com.ymzhao.website.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @date 2019/4/25 11:46
* @atuther net source
*/
public class JwtUtil {
//密钥
public static final String SECRET = "sgfdsopljkhsl;o437632";
//过期时间:秒
public static final int EXPIRE = 60 * 1;
/**
* 生成Token
* @param username
* @return
* @throws Exception
*/
public static String createToken(String username) throws Exception {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.SECOND, EXPIRE);
Date expireDate = nowTime.getTime();
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
String token = JWT.create()
.withHeader(map)//头
.withClaim("username", username)
.withSubject("测试")//
.withIssuedAt(new Date())//签名时间
.withExpiresAt(expireDate)//过期时间
.sign(Algorithm.HMAC256(SECRET));//签名
return token;
}
/**
* 验证Token
* @param token
* @return
* @throws Exception
*/
public static Map<String, Claim> verifyToken(String token)throws Exception{
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT jwt = null;
try {
jwt = verifier.verify(token);
}catch (Exception e){
throw new RuntimeException("凭证已过期,请重新登录");
}
return jwt.getClaims();
}
/**
* 解析Token
* @param token
* @return
*/
public static Map<String, Claim> parseToken(String token){
DecodedJWT decodedJWT = JWT.decode(token);
return decodedJWT.getClaims();
}
}
验证方法返回类为Map<String, Claim>,Claim定义如下:
有需要的话,使用它提供的格式转换方法
3.token生成
demo 如下,直接调用上一节中的token生成方法:
@Service
public class LoginServiceImpl implements LoginService {
@Override
public String logIn(String username, String password) {
// 可封装下返回类,避免直接抛异常,结构为{code: 0, data: "xxx", message: "成功"}
if("admin".equals(username) && "123abc".equals(password)) {
String token;
try {
token = JwtUtil.createToken(username);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("生成token失败");
}
return token;
}
throw new RuntimeException("用户名/密码错误");
}
}
返回给前端后,需同前端约定将token置于请求头中,以实现用户登录验证
4.jwt拦截器
拦截http请求,检查token的有效与否
package com.ymzhao.website.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.interfaces.Claim;
import com.ymzhao.website.utils.JwtUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* Create By ymzhao on 2021/3/11
*/
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
String uri = request.getRequestURI();
System.out.println("uri: " + uri);
String headToken = request.getHeader("Authorization");
if(StringUtils.isEmpty(headToken)) {
Map<String, Object> map = new HashMap<>();
map.put("code", 20001);
map.put("message", "Missing or invalid Authorization header");
ErrorResponse(response, map);
return false;
}
try {
Map<String, Claim> map = JwtUtil.verifyToken(headToken);
} catch (Exception e) {
e.printStackTrace();
Map<String, Object> map = new HashMap<>();
map.put("code", 20002);
map.put("message", "Invalid Authorization header " + e.getLocalizedMessage());
ErrorResponse(response, map);
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
// 被拦截的请求响应
private void ErrorResponse(HttpServletResponse response, Map<String, Object> result){
OutputStream out = null;
JSONObject object = new JSONObject();
object.put("result", result);
try{
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
out = response.getOutputStream();
out.write(object.toString().getBytes());
out.flush();
}catch (Exception e){
e.printStackTrace();
}finally{
try {
if (out != null) {
out.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
5.jwt拦截器配置
配置拦截模式
此处踩了三个坑:静态资源也被拦截了(三、1);项目/error路径也会被拦截(三、2);yml中设置了项目路径时,拦截路径中需要加上该路径(三、3)
package com.ymzhao.website.config;
import com.ymzhao.website.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;
/**
* Create By ymzhao on 2021/3/11
*/
@Configuration
public class JwtInterceptorConfig extends WebMvcConfigurationSupport {
@Resource
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,要声明拦截器对象和要拦截的请求
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") //所有路径都被拦截
.excludePathPatterns("/login/**") // 排除用户登录请求
.excludePathPatterns("/register/**") // 排除用户注册请求
.excludePathPatterns("/error");
super.addInterceptors(registry);
}
/**
* 用来指定静态资源不被拦截,否则继承WebMvcConfigurationSupport这种方式会导致静态资源无法直接访问
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
super.addResourceHandlers(registry);
}
}
二、jwt前端配置
同后端的约定,登录后,返回的token存放在之后的请求头中,属性名为Authoritization
1.获取token
使用封装的axios方法(见下一节)发送请求
this.fetchPost("/zymwb/login/log_in", {username: 'admin', password: '123abc'}).then(res => {
console.log(res)
const result = res.data // 示例中返回的result结构为{code: 0, data: 'xxx', message: '成功'}
if(result != null && result.code == 0) { // 登录成功
localStorage.setItem('token', result.data)
} else { // 登录失败
}
});
2.封装axios
import Vue from 'vue';
import axios from 'axios';
import qs from 'qs';
const host = 'http://192.168.0.179';
const baseURL = host + ':13145';
axios.defaults.withCredentials = false;
axios.defaults.timeout = 2500;
// 请求拦截
axios.interceptors.request.use(
config => {
const token = localStorage.getItem("token")
config.headers.Authoritization = token
return config;
},
err => {
return Promise.reject(err);
}
)
// 响应拦截
axios.interceptors.response.use(
res => {
return res;
},
err => {
return Promise.reject(err);
}
)
const fetch = (url, method, data) => {
data = data ? data : {};
let httpDefaultOpts = { //http默认配置
method: method,
url: baseURL + url,
params:data,
data:qs.stringify(data),
headers: method=='get'?{
"Accept": "application/json",
"Content-Type": "application/json; charset=UTF-8"
}:{
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
}
if(method =='get'){
delete httpDefaultOpts.data;
}else{
delete httpDefaultOpts.params;
}
let promise = new Promise(function(resolve, reject) {
axios(httpDefaultOpts).then(
res => {
resolve(res);
}
).catch(
res => {
reject(res);
}
)
})
return promise;
}
const fetchGet = (url, data) => {
return fetch(url, "get", data);
}
const fetchPost = (url, data) => {
return fetch(url, "post", data);
}
Vue.prototype.fetchGet = fetchGet;
Vue.prototype.fetchPost = fetchPost;
三、踩坑记录
1.静态资源被拦截(一、5)
BUG记录:SpringBoot项目配置拦截器,部署后出现无法调用接口的问题
2./error被拦截(一、5)
springboot的错误处理url为/error也会被拦截,需要添加排除
3.yml项目路径(一、5)
yml中设置了项目路径时,拦截路径中需要加上该路径(三、3)
server:
port: 13145
servlet:
context-path: /zymwb
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/zymwb/**") //所有路径都被拦截
.excludePathPatterns("/zymwb/login/**") // 排除用户登录请求