本章解决分布式单点登录的问题,一般的情况下我们会通过维护session的方式去做单点为此还需要维护session一致性的问题,本章我将通过redis+token+自定义注解的方式去实现.
本章的前提是需要java基础及redis的基本使用,redis与springboot集成在上篇中有讲到
https://blog.csdn.net/weixin_40685388/article/details/97567432
思路 :
1.当用户进行登录的时候,登录成功我们会把token设置到redis中并设置过期时间后返回给前端
2.每次前端来请求数据的时候,我们会通过AuthCheckInterceptor继承 HandlerInterceptorAdapter 类
的方式, 在请求controller之前通过反射的方法判断用户请求的类或方法是否有@Auth (自定义)注解,
如果用户请求的方法有此注解,我们将获取请求头中设置的token,获取到token后我们将去redis中获取用户信息并放行,
如果redis中不存在token我们可返回状态码 或异常信息。
3.在此同时我们需要自定义异常来实现异常的转换机制,以便响应出错误的token信息
Demo的地址,如有需要可自行下载:https://gitee.com/Audis/ccl-coding-sso.git
项目目录结构
1.构建自定义注解,在接口或者类设置此注解,就代表需要登录认证
/**
* @author CHANG
* @date 2019/7/28 11:02
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Auth {
}
设置请求上下文通过 ThreadLocal将请求与响应隔离,为此可在请求时与响应结束时可获取同一个请求的id,在此可将用户的token设置在此,在请求的时候用户携带的token可在全局获取用户的信息
package com.ccl.coding.sso.context;
import com.ccl.coding.sso.domain.dto.UserContextDto;
import com.ccl.coding.sso.utils.ConfigConstants;
import com.ccl.coding.sso.utils.FastJSONUtil;
import com.ccl.coding.sso.utils.RedisConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* @author CHANG
* @date 2019/7/28 11:05
*/
@Component
public class RequestContext {
private static StringRedisTemplate redisTemplate;
private static ThreadLocal<HttpServletRequest> requestContext = new ThreadLocal<HttpServletRequest>();
private static ThreadLocal<HttpServletResponse> responseContext = new ThreadLocal<HttpServletResponse>();
public static void init(HttpServletRequest request, HttpServletResponse response, Object controller) {
setRequest(request);
setResponse(response);
}
public static HttpSession getSession() {
if (getRequest() == null) {
return null;
}
return getRequest().getSession(true);
}
public static HttpServletRequest getRequest() {
return requestContext.get();
}
public static HttpServletResponse getResponse() {
return responseContext.get();
}
public static void setRequest(HttpServletRequest request) {
requestContext.set(request);
}
public static void setResponse(HttpServletResponse response) {
responseContext.set(response);
}
public static void clear() {
requestContext.remove();
responseContext.remove();
}
public static String getToken() {
String token = getRequest().getHeader(ConfigConstants.HTTP_HEADER_AUTH_TOKEN);
return token;
}
/**
* 获取当前用户
*
* @return
*/
public static UserContextDto getCurrentManagerUserContext() {
String token = getToken();
if (StringUtils.isEmpty(token)) {
return null;
}
String json = redisTemplate.opsForValue().get(RedisConstants.getUserTokenKey(token));
if (StringUtils.isEmpty(json)) {
return null;
}
return FastJSONUtil.parsePojo(json, UserContextDto.class);
}
/**
* 获取当前用户id
*
* @return
*/
public static Long getCurrentManagerUserId() {
UserContextDto managerUserContextDto = getCurrentManagerUserContext();
if (managerUserContextDto == null) {
return null;
}
return managerUserContextDto.getUserId();
}
@Autowired
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
RequestContext.redisTemplate = redisTemplate;
}
}
拦截器对上下文进行请求初始化及响应后销毁
package com.ccl.coding.sso.interceptor;
import com.ccl.coding.sso.context.RequestContext;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author CHANG
* @date 2019/7/28 11:04
*/
public class RequestContextInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
RequestContext.init(request,response,handler);
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestContext.clear();
super.afterCompletion(request, response, handler, ex);
}
}
对请求的路径进行校验
package com.ccl.coding.sso.confing;
import com.ccl.coding.sso.interceptor.AuthCheckInterceptor;
import com.ccl.coding.sso.interceptor.RequestContextInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author CHANG
* @date 2019/7/28 11:01
*/
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
@Autowired
private AuthCheckInterceptor authCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestContextInterceptor()).addPathPatterns("/**");
registry.addInterceptor(authCheckInterceptor).addPathPatterns("/**");
}
}
2.通过拦截器效验token
package com.ccl.coding.sso.interceptor;
import com.ccl.coding.sso.annotation.Auth;
import com.ccl.coding.sso.context.RequestContext;
import com.ccl.coding.sso.exception.TokenExpireException;
import com.ccl.coding.sso.utils.ConfigConstants;
import com.ccl.coding.sso.utils.RedisConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* @author CHANG
* @date 2019/7/28 11:01
*/
@Component
@Slf4j
public class AuthCheckInterceptor extends HandlerInterceptorAdapter {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (StringUtils.equalsIgnoreCase(request.getMethod(), "OPTIONS")) {
return true;
}
String token = RequestContext.getRequest().getHeader(ConfigConstants.HTTP_HEADER_AUTH_TOKEN);
if (StringUtils.isNotEmpty(token)) {
String tokenKey = RedisConstants.getUserTokenKey(token);
boolean hasTokenKey = redisTemplate.hasKey(tokenKey);
if (hasTokenKey) {
redisTemplate.expire(tokenKey,
RedisConstants.CCL_CODING_SSO_TOKEN_TIME, TimeUnit.SECONDS);
}
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Auth classAuth = handlerMethod.getBean().getClass().getAnnotation(Auth.class);
if (classAuth == null) {
Auth auth = handlerMethod.getMethod().getAnnotation(Auth.class);
if (auth == null) {
return super.preHandle(request, response, handler);
}
}
if (StringUtils.isEmpty(token)) {
throw new TokenExpireException("token can not be null!");
}
String json = redisTemplate.opsForValue().get(RedisConstants.getUserTokenKey(token));
if (StringUtils.isEmpty(json)) {
throw new TokenExpireException("token expire!");
}
}
return super.preHandle(request, response, handler);
}
}
3.编写controller登录接口
package com.ccl.coding.sso.controller;
import com.ccl.coding.sso.domain.entity.User;
import com.ccl.coding.sso.domain.dto.UserContextDto;
import com.ccl.coding.sso.domain.dto.UserLoginDto;
import com.ccl.coding.sso.domain.protocol.ApiResponse;
import com.ccl.coding.sso.utils.FastJSONUtil;
import com.ccl.coding.sso.utils.RedisConstants;
import com.ccl.coding.sso.utils.UUIDGenerator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author CHANG
* @date 2019/7/28 11:24
*/
@RestController
@RequestMapping("user")
public class UserController extends BaseController {
/**
* 登录
*
* @param userLoginDto
* @return
*/
@PostMapping("login")
public ApiResponse login(@RequestBody UserLoginDto userLoginDto) {
String username = userLoginDto.getUsername();
String password = userLoginDto.getPassword();
User user = getUser(username, password);
if (Objects.nonNull(user)) {
return success(createUserContext(user));
}
return success();
}
/**
* 用户密码校验成功后设置token
*
* @param user
* @return
*/
private UserContextDto createUserContext(User user) {
String token = UUIDGenerator.generate();
UserContextDto managerUserContextDto = new UserContextDto(user, token);
redisTemplate.opsForValue().set(RedisConstants.getUserTokenKey(token),
FastJSONUtil.setJsonString(managerUserContextDto),
RedisConstants.CCL_CODING_SSO_TOKEN_TIME, TimeUnit.SECONDS);
return managerUserContextDto;
}
/**
* 模拟数据库
*
* @param username
* @param password
* @return
*/
private User getUser(String username, String password) {
if ("admin".equalsIgnoreCase(username) && "admin".equalsIgnoreCase(password)) {
return User.builder()
.id(100L)
.username(username)
.password(password)
.email("[email protected]")
.mobile("123456789")
.build();
}
return null;
}
}
通过请求上下文获取用户的信息
package com.ccl.coding.sso.controller;
import com.ccl.coding.sso.context.RequestContext;
import com.ccl.coding.sso.domain.entity.User;
import com.ccl.coding.sso.domain.dto.UserContextDto;
import com.ccl.coding.sso.domain.protocol.ApiResponse;
import com.ccl.coding.sso.utils.FastJSONUtil;
import com.ccl.coding.sso.utils.RedisConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class BaseController {
@Autowired
protected StringRedisTemplate redisTemplate;
protected String getToken() {
return RequestContext.getToken();
}
/**
* 获取当前用户
*
* @return
*/
protected UserContextDto getCurrentManagerUserContext() {
return RequestContext.getCurrentManagerUserContext();
}
/**
* 获取当前用户id
*
* @return
*/
protected Long getCurrentManagerUserId() {
return RequestContext.getCurrentManagerUserId();
}
/**
* 获取当前用户手机号
*
* @return
*/
protected String getCurrentManagerUserMobile() {
return RequestContext.getCurrentManagerUserContext().getMobile();
}
/**
* 刷新当前用户信息
*
* @return
*/
protected UserContextDto refreshCurrentManagerUser(User user) {
String token = getToken();
UserContextDto userContextDto = new UserContextDto(user, token);
redisTemplate.opsForValue().set(RedisConstants.getUserTokenKey(token),
FastJSONUtil.setJsonString(userContextDto),
RedisConstants.CCL_CODING_SSO_TOKEN_TIME, TimeUnit.SECONDS);
return userContextDto;
}
/**
* 清除当前用户,退出登陆
*
* @return
*/
protected void clearCurrentUser() {
String token = getToken();
redisTemplate.delete(RedisConstants.getUserTokenKey(token));
}
protected <T> ApiResponse<T> success(T date) {
return ApiResponse.build().success(date);
}
protected ApiResponse success() {
return success(null);
}
}
package com.ccl.coding.sso.controller;
import com.ccl.coding.sso.annotation.Auth;
import com.ccl.coding.sso.domain.dto.UserContextDto;
import com.ccl.coding.sso.domain.dto.UserLoginDto;
import com.ccl.coding.sso.domain.protocol.ApiResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author CHANG
* @date 2019/7/28 12:20
*/
@RestController
@RequestMapping("admin")
@Auth
public class AdminController extends BaseController {
/**
* 获取数据
* @return
*/
@GetMapping("getUser")
public ApiResponse getLoingUser() {
UserContextDto currentManagerUserContext = getCurrentManagerUserContext();
return success(currentManagerUserContext);
}
}
工具类
package com.ccl.coding.sso.utils;
/**
* @author CHANG
* @date 2019/7/28 11:06
*/
public class ConfigConstants {
public static final String HTTP_HEADER_AUTH_TOKEN = "Auth-Token";
}
package com.ccl.coding.sso.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
@Slf4j
public class FastJSONUtil {
public static <T> T parsePojo(String jsonString, Class<T> cls) {
T t = null;
try {
t = JSON.parseObject(jsonString, cls);
} catch (Exception e) {
log.error("parsePojo error in FastJSONUtil", e);
}
return t;
}
/**
* parse
*
* @param jsonString
* @return
*/
public static Map<String, Object> parseMap(String jsonString) {
try {
Map<String, Object> jsonObject = JSONObject.parseObject(jsonString);
return jsonObject;
} catch (Exception e) {
log.error("parsePojo error in FastJSONUtil", e);
}
return null;
}
public static <T> List<T> parsePojoList(String jsonString, Class<T> cls) {
List<T> tList = null;
try {
tList = JSON.parseArray(jsonString, cls);
} catch (Exception e) {
log.error("parsePojoList error in FastJSONUtil", e);
}
return tList;
}
public static String setJsonString(Object o) {
String jsonString = JSON.toJSONString(o);
return jsonString;
}
}
package com.ccl.coding.sso.utils;
/**
* @author CHANG
* @date 2019/7/28 11:07
*/
public class RedisConstants {
// 用户
public static final String CCL_CODING_SSO_TOKEN = "ccl:coding:sso:token:";
// 单位 s
public static final int CCL_CODING_SSO_TOKEN_TIME = 60 * 60 * 24 * 3;
public static String getUserTokenKey(String token) {
return CCL_CODING_SSO_TOKEN + token;
}
}
package com.ccl.coding.sso.utils;
import java.util.UUID;
public class UUIDGenerator {
/**
* UUID 字符串生成
*
* @return
*/
public static String generate() {
String uuid = UUID.randomUUID().toString();
return uuid.replaceAll("-", "");
}
}
实体类
package com.ccl.coding.sso.domain.dto;
import com.ccl.coding.sso.domain.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.springframework.beans.BeanUtils;
import java.io.Serializable;
/**
* @author CHANG
* @date 2019/7/28 11:12
*/
@Data
public class UserContextDto implements Serializable {
private String token;
private ManagerUserInfo managerUser;
public UserContextDto() {
}
public UserContextDto(User u, String token) {
this.token = token;
this.managerUser = new ManagerUserInfo();
BeanUtils.copyProperties(u, this.managerUser);
}
public Long getUserId() {
return managerUser.getId();
}
public String getMobile() {
return managerUser.getMobile();
}
@Data
static class ManagerUserInfo {
private Long id;
/**
* 用户名
*/
private String username;
/**
* 手机号
*/
private String mobile;
/**
* 邮箱
*/
private String email;
public ManagerUserInfo() {
}
}
}
package com.ccl.coding.sso.domain.entity;
import lombok.Builder;
import lombok.Data;
/**
* @author CHANG
* @date 2019/7/28 0:04
*/
@Data
@Builder
public class User {
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 手机号
*/
private String mobile;
/**
* 邮箱
*/
private String email;
}