Redis(二) Java集成Redis之分佈式單點登錄(SSO)

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

 

5.驗證結果

登錄成功後返回信息

 根據登錄成功返回的token獲取用戶的信息

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章