我们平时开发的程序一般都会涉及到用户模块,有时候的业务必须要用户登录才能实现,涉及到登录那么就不可避免的要涉及到鉴权,那么一个简单的鉴权应该怎么做?下面来介绍一下。
主要思路
在用户登录的时候返回一个token给前端,往后前端的每次请求都用请求拦截器拦截,并且在请求头添加一个 Authorization ,值就是之前后端返回的token,后端对每次请求进行拦截并且鉴权。
实现
前端
下面是axios的实例化和对应的请求拦截器。
const service = axios.create({
baseURL: "http://localhost:8080", // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
headers: {
"content-type": "application/x-www-form-urlencoded"
},
withCredentials: true, // 允许携带凭证
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
if (getToken()) {
config.headers['Authorization'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
对token的操作我们可以封装成函数
const TokenKey = 'token'
export function getToken() {
return localStorage.getItem(TokenKey);
}
export function setToken(token) {
return localStorage.setItem(TokenKey, token);
}
export function removeToken() {
return localStorage.removeItem(TokenKey);
}
上面就是前端的实现啦,简单点来说前端只负责接收token并且储存,在每次发请求的时候带上就行
后端
后端鉴权的机制比较多,我们首先来想一下,token如何来?里面的内容是什么?答案是用jwt等安全框架造出来,里面的内容可以我们自己来定(比如储存好用户的id,过期时间等)
ok,有了上面的基础,我们可以猜到肯定需要一个工具类来完成加密信息产生token,解密token,验证token,刷新token等。
下面是工具类代码(需要加密什么信息可以自己添加,算法也比较基础,可以更换加密机制)
package com.sky.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private static final int EXPIRATION_HOURS = 24;
private static final String SECRET = "demoCat";
/**
* 生成token
*
* @param userId 用户id
* @param expiresInHours 有效时间
* @return JWT token
*/
public static String createToken(Long userId, int expiresInHours) {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.HOUR, expiresInHours);
Date expiresDate = nowTime.getTime();
Map<String, Object> map = new HashMap<>();
map.put("typ", "JWT");
map.put("alg", "HS256");
return JWT.create()
.withHeader(map)
.withClaim("id", userId)
.withExpiresAt(expiresDate)
.withIssuedAt(new Date())
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 验证token是否过期
*
* @param token JWT token
* @return true 如果 token 有效,否则 false
*/
public static Boolean verifyToken(String token) {
if (token == null || token.isEmpty()) {
return false; // Token 为空
}
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
Date expirationDate = jwt.getExpiresAt();
Date currentDate = new Date();
return !expirationDate.before(currentDate);
} catch (JWTVerificationException e) {
return false;
}
}
/**
* 解析token
*
* @param token JWT token
* @return 包含 JWT 令牌声明的映射
* @throws Exception 如果解析失败
*/
public static Map<String, Claim> parseToken(String token) throws Exception {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaims();
}
/**
* 刷新token
*
* @param token JWT token
* @return 刷新后的 JWT token
*/
public static String refreshToken(String token) {
try {
DecodedJWT decodedJWT = JWT.decode(token);
Long userId = decodedJWT.getClaim("id").asLong();
return createToken(userId, EXPIRATION_HOURS);
} catch (JWTVerificationException e) {
// Token 解码失败或签名验证失败,处理异常
return null;
}
}
}
ok,有了这个工具,下面就要来编写我们的业务了。
首先我们每次都要拦截,那么肯定需要一个拦截器了,我们先来写一个拦截器并且注册到系统中
package com.sky.interceptor;
import com.auth0.jwt.interfaces.Claim;
import com.sky.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* jwt令牌校验的拦截器
* 相当于系统的锁
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TODO 写一篇博客如何做一个权限系统shiro(权限五表,包括vip也可以直接用的那种)
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
if ("/user/login".equals(request.getRequestURI())
||"/upload".equals(request.getRequestURI())) {
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader("Authorization");
Boolean loggedIn = JwtUtil.verifyToken(token);
//2、校验令牌
try {
//TODO 思考如何为系统搭建一个日志系统,如何打日志为佳,
log.info("jwt校验:{}", token);
if (token != null && loggedIn) {
Map<String, Claim> stringClaimMap = JwtUtil.parseToken(token);
Long userId = stringClaimMap.get("id").asLong();
request.setAttribute("current_user", userId.intValue());
System.out.println("当前用户id:"+userId);
return true;
}else {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Unauthorized access");
}
//3、通过,放行
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(403);
return false;
}
}
}
然后注册拦截器
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
到目前为止,我们的鉴权就初步打造完成啦,还有最后一步就是用户登录的时候发令牌给用户。这个我们结合自己的业务就行啦。
上面就是我们实现的简单的鉴权系统啦,为什么简单?因为我们还可以在这个基础上加入权限(比如vip,管理员等),加密算法也可以更完善,还有token也没有实现刷新机制。
当然,这些都是随业务的变化而变化的。我们知道了基础,要用的时候再加就行啦!
今天的鉴权就到这里,下次见!