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获取用户的信息

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