上一节讲了如何使用JWT生成令牌,下面说说单体服务认证基本流程。
认证流程
流程图:
流程描述:
- 用户输入登录信息,客户端(Web/APP等)发起登录请求;
- 服务端校验该用户是否有效,用户有效则返回令牌信息,用户无效则返回用户不存在;
- 客户端收到令牌信息后,将其存储到客户端(如:cookie中);
- 客户端端请求服务器资源数据,在有头信息中携带令牌信息;
- 服务器拦截器校验该令牌是否有效,有效的话则返回请求的资源数据;
- 若令牌认证失败401,则携带刷新令牌,请求刷新令牌的接口;
- 服务端校验该刷新令牌是否有效,有效的话则返回新的令牌信息给客户端;
- 无效的话,则返回刷新令牌过期402给客户端,客户端跳转到用户登录页;
- 用户重新登录。
如何实现
- 集成上一节令牌SDK
<dependency>
<groupId>com.angel.ocean</groupId>
<artifactId>ocean-auth-core</artifactId>
<version>1.0.0</version>
</dependency>
- 定义拦截器 AuthFilter,去校验令牌是否有效,认证失败则返回401
package com.angel.ocean.filter;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.angel.ocean.common.ApiResult;
import com.angel.ocean.constant.NoAuthUrl;
import com.angel.ocean.constant.ResultCode;
import com.angel.ocean.constant.redis.RedisCacheKey;
import com.angel.ocean.token.TokenUtil;
import com.angel.ocean.util.SecurityUtil;
import com.angel.ocean.util.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.FilterConfig;
import java.io.IOException;
/**
1. 认证Filter
*/
@Slf4j
public class AuthFilter implements Filter {
@Resource
private RedissonClient redissonClient;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
ApplicationContext context = WebApplicationContextUtils
.getWebApplicationContext(filterConfig.getServletContext());
this.redissonClient = context.getBean(RedissonClient.class);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 放开不需要认证的API
String apiUrl = ServletUtil.getRequest().getRequestURI();
if(NoAuthUrl.contains(apiUrl)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// Token有效性校验
String token = SecurityUtil.getToken();
if(StrUtil.isEmpty(token)) {
// 未携带令牌,认证失败返回401
responseWrite(servletResponse, ApiResult.error(ResultCode.UNAUTHORIZED));
return;
}
if((TokenUtil.isExpired(token))) {
// 携带令牌但是该令牌已过期,认证失败返回401
responseWrite(servletResponse, ApiResult.error(ResultCode.UNAUTHORIZED));
return;
}
Long userId = SecurityUtil.getUserId();
RBucket<String> accessTokenCache = redissonClient.getBucket(RedisCacheKey.accessTokenKey(userId));
if(!accessTokenCache.isExists()) {
// 携带令牌,用户已退出登录导致令牌失效,认证失败返回401
responseWrite(servletResponse, ApiResult.error(ResultCode.UNAUTHORIZED));
return;
}
String oldToken = accessTokenCache.get();
if(!oldToken.equals(token)) {
// 该用户已经有了新令牌,老令牌过期了,使用老令牌认证,认证失败返回401
responseWrite(servletResponse, ApiResult.error(ResultCode.UNAUTHORIZED));
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void responseWrite(ServletResponse response, Object data) {
try {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(data));
} catch (IOException e) {
log.error("responseWrite() error:", e);
}
}
}
- 定义与认证和用户有关的相关接口
- 登录接口
- 获取用户信息的接口
- 刷新令牌的接口
- 用户退出接口
package com.angel.ocean.controller;
import com.angel.ocean.common.ApiResult;
import com.angel.ocean.domain.dto.LoginDTO;
import com.angel.ocean.service.AuthService;
import com.angel.ocean.token.model.TokenInfo;
import com.angel.ocean.token.model.UserInfo;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 鉴权认证相关
*/
@RestController
@RequestMapping("user")
public class AuthController {
@Resource
private AuthService authService;
/**
* 用户登录
* @param dto
* @return
*/
@PostMapping("login")
public ApiResult<TokenInfo> login(@RequestBody LoginDTO dto) {
return authService.login(dto);
}
/**
* 获取用户信息
* @return
*/
@GetMapping("info")
public ApiResult<UserInfo> userInfo() {
return authService.userInfo();
}
/**
* 刷新token
* @param refreshToken
* @return
*/
@PostMapping("token/refresh")
public ApiResult<TokenInfo> refreshToken(@RequestBody String refreshToken) {
return authService.refreshToken(refreshToken);
}
/**
* 用户退出
* @return
*/
@PostMapping("logout")
public ApiResult logout() {
return authService.logout();
}
}
业务实现
package com.angel.ocean.service.impl;
import cn.hutool.core.util.StrUtil;
import com.angel.ocean.common.ApiResult;
import com.angel.ocean.constant.ResultCode;
import com.angel.ocean.constant.redis.RedisCacheKey;
import com.angel.ocean.domain.dto.LoginDTO;
import com.angel.ocean.domain.entity.SysUser;
import com.angel.ocean.mapper.SysUserMapper;
import com.angel.ocean.token.TokenUtil;
import com.angel.ocean.token.model.TokenInfo;
import com.angel.ocean.token.model.UserInfo;
import com.angel.ocean.util.SecurityUtil;
import com.angel.ocean.service.AuthService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
/**
1. 鉴权认证相关
*/
@Service
public class AuthServiceImpl implements AuthService {
@Resource
private RedissonClient redissonClient;
@Resource
private SysUserMapper sysUserMapper;
@Override
public ApiResult<TokenInfo> login(LoginDTO dto) {
// 参数非空校验
if(StrUtil.isEmpty(dto.getUsername()) || StrUtil.isEmpty(dto.getPassword())) {
return ApiResult.error(ResultCode.PARAM_ERROR);
}
QueryWrapper<SysUser> query = new QueryWrapper<>();
query.eq("username", dto.getUsername());
query.eq("password", dto.getPassword());
SysUser sysUser = sysUserMapper.selectOne(query);
if(null == sysUser) {
return ApiResult.error(ResultCode.USER_NOT_EXIST);
}
UserInfo userInfo = new UserInfo();
userInfo.setName(sysUser.getUsername());
userInfo.setUid(sysUser.getId());
TokenInfo tokenInfo = TokenUtil.generateToken(userInfo);
setTokenCache(sysUser.getId(), tokenInfo);
return ApiResult.success(tokenInfo);
}
@Override
public ApiResult<UserInfo> userInfo() {
UserInfo userInfo = SecurityUtil.getLoginUser();
return ApiResult.success(userInfo);
}
@Override
public ApiResult<TokenInfo> refreshToken(String refreshToken) {
// 刷新令牌有效性校验
if(TokenUtil.isExpired(refreshToken)) {
return ApiResult.error(ResultCode.AUTHORIZED_EXPIRED);
}
TokenInfo tokenInfo = TokenUtil.refreshToken(refreshToken);
Long userId = TokenUtil.getUserInfoByToken(refreshToken).getUid();
setTokenCache(userId, tokenInfo);
return ApiResult.success(tokenInfo);
}
@Override
public ApiResult logout() {
Long userId = SecurityUtil.getUserId();
if(null != userId) {
removeTokenCache(userId);
}
return ApiResult.success();
}
/**
* 设置令牌缓存
* @param userId
* @param tokenInfo
*/
private void setTokenCache(Long userId, TokenInfo tokenInfo) {
RBucket<String> accessTokenCache = redissonClient.getBucket(RedisCacheKey.accessTokenKey(userId));
accessTokenCache.set(tokenInfo.getAccessToken());
accessTokenCache.expireAt(new Date(tokenInfo.getAccessTokenExpireIn() * 1000));
RBucket<String> refreshTokenCache = redissonClient.getBucket(RedisCacheKey.refreshTokenKey(userId));
refreshTokenCache.set(tokenInfo.getRefreshToken());
refreshTokenCache.expireAt(new Date(tokenInfo.getRefreshTokenExpireIn() * 1000));
}
/**
* 删除令牌缓存
* @param userId
*/
private void removeTokenCache(Long userId) {
RBucket<String> accessTokenCache = redissonClient.getBucket(RedisCacheKey.accessTokenKey(userId));
accessTokenCache.delete();
RBucket<String> refreshTokenCache = redissonClient.getBucket(RedisCacheKey.refreshTokenKey(userId));
refreshTokenCache.delete();
}
}
- 用户信息的工具类SecurityUtil,给其他业务使用
package com.angel.ocean.util;
import com.angel.ocean.token.TokenUtil;
import com.angel.ocean.token.model.UserInfo;
public class SecurityUtil {
private SecurityUtil() {
}
public static String getUsername() {
UserInfo userInfo = getLoginUser();
String username = userInfo.getName();
return ServletUtil.urlDecode(username);
}
public static Long getUserId() {
UserInfo userInfo = getLoginUser();
return userInfo.getUid();
}
public static UserInfo getLoginUser() {
UserInfo userInfo = TokenUtil.getUserInfoByToken(getToken());
return userInfo;
}
public static String getToken() {
String token = ServletUtil.getRequest().getHeader("Authorization");
return token;
}
}
package com.angel.ocean.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
public class ServletUtil {
private ServletUtil() {
}
public static String getParameter(String name) {
return getRequest().getParameter(name);
}
public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue);
}
public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name));
}
public static Integer getParameterToInt(String name, Integer defaultValue) {
return Convert.toInt(getRequest().getParameter(name), defaultValue);
}
public static HttpServletRequest getRequest() {
try {
return getRequestAttributes().getRequest();
} catch (Exception var1) {
return null;
}
}
public static HttpServletResponse getResponse() {
try {
return getRequestAttributes().getResponse();
} catch (Exception var1) {
return null;
}
}
public static HttpSession getSession() {
return getRequest().getSession();
}
public static ServletRequestAttributes getRequestAttributes() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes)attributes;
} catch (Exception var1) {
return null;
}
}
public static Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while(enumeration.hasMoreElements()) {
String key = (String)enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
}
return map;
}
public static String renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException var3) {
var3.printStackTrace();
}
return null;
}
public static boolean isAjaxRequest(HttpServletRequest request) {
String accept = request.getHeader("accept");
if (accept != null && accept.indexOf("application/json") != -1) {
return true;
} else {
String xRequestedWith = request.getHeader("X-Requested-With");
if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) {
return true;
} else {
String uri = request.getRequestURI();
if (StrUtil.containsAnyIgnoreCase(uri, new String[]{".json", ".xml"})) {
return true;
} else {
String ajax = request.getParameter("__ajax");
return StrUtil.containsAnyIgnoreCase(ajax, new String[]{"json", "xml"});
}
}
}
}
public static String urlDecode(String str) {
try {
return URLDecoder.decode(str, "UTF-8");
} catch (UnsupportedEncodingException var2) {
return "";
}
}
}
运行演示
- 登录接口
注意:由于是演示使用,用户名字是明文(不安全),在项目开发过程中,一定要先在客户端加密,然后服务区解密。
- 刷新令牌接口
- 获取用户信息接口,携带有效令牌
- 获取用户信息, 携带无效令牌