本章解決分佈式單點登錄的問題,一般的情況下我們會通過維護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;
}