Security中使用Redis管理会话(模拟cookie实现)

news2025/1/22 12:41:23

在这里插入图片描述

配置redis相关

1. 配置Redis

package com.zzhua.blog.config.redis;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
import com.zzhua.blog.config.security.UserDetailDTO;
import com.zzhua.blog.config.security.session.SessionInformationMixin;
import com.zzhua.blog.util.JsonUtil;
import com.zzhua.blog.util.ser.DateJsonDeserializer;
import com.zzhua.blog.util.ser.DateJsonSerializer;
import org.junit.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.StringRedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.security.core.session.SessionInformation;
import springfox.documentation.spring.web.json.Json;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

@Configuration
public class RedisConfig {

    /* 方便将java对象的类型也写入json, 同时, 将json直接读取成java对象, 得益于ObjectMapper的defaultTyping支持, 但json不具备通用性 */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        RedisSerializer<String> stringRedisSerializer = RedisSerializer.string();

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        // 配置ObjectMapper
        ObjectMapper mapper = new ObjectMapper();
        // 写入类型到json中, 方便反序列化回来
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        // 遇到未知属性, 不报错
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        // 处理 SessionInformation 反序列化报错的问题(SessionInformation里面没有无参构造方法)
        mapper.addMixIn(SessionInformation.class, SessionInformationMixin.class);
        // 不要写成时间戳
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setTimeZone(TimeZone.getDefault());

        jackson2JsonRedisSerializer.setObjectMapper(mapper);

        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        return redisTemplate;
    }

    /* 方便在redis中读写通用可读的json, 但需要自己手动把对象转为json, 并将json字符串反序列化为java对象 */
    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(redisConnectionFactory);
        stringRedisTemplate.setKeySerializer(RedisSerializer.string());
        stringRedisTemplate.setValueSerializer(RedisSerializer.string());
        stringRedisTemplate.setHashKeySerializer(RedisSerializer.string());
        stringRedisTemplate.setHashValueSerializer(RedisSerializer.string());
        return stringRedisTemplate;
    }

}

2. RedisService工具

package com.zzhua.blog.config.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /* key 是否存在 */
    public Boolean existKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /* 设置 key 失效时间 */
    public Boolean expireKey(String key, long timeInSeconds) {
        return redisTemplate.expire(key, timeInSeconds, TimeUnit.SECONDS);
    }

    /* 移除 key */
    public Boolean removeKey(String key) {
        return redisTemplate.delete(key);
    }

    /* 移除多个 key */
    public Boolean removeKeys(Collection<String> keys) {
        return redisTemplate.delete(keys) > 0;
    }

    public Long incr(String key) {
        return redisTemplate.opsForValue().increment(key);
    }

    public Long incr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /* 获取满足pattern的key */
    public Set<String> getKeys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /* 存入对象 */
    public void set(String key, Object obj, long timeInSeconds) {
        redisTemplate.opsForValue().set(key, obj, timeInSeconds, TimeUnit.SECONDS);
    }

    public void set(String key, Object obj) {
        redisTemplate.opsForValue().set(key, obj);
    }

    public <T> T get(String key) {
        return (T)redisTemplate.opsForValue().get(key);
    }

    /* 存入json字符串 */
    public void setContent(String key, String content, long timeInSeconds) {
        stringRedisTemplate.opsForValue().set(key, content, timeInSeconds, TimeUnit.SECONDS);
    }

    public void setContent(String key, String content) {
        stringRedisTemplate.opsForValue().set(key, content);
    }

    public String getContent(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }
}

3. RedisSessionRegistry会话注册中心

package com.zzhua.blog.config.security.session;

import com.zzhua.blog.config.redis.RedisConstants;
import com.zzhua.blog.config.redis.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

@Component
public class RedisSessionRegistry implements SessionRegistry, ApplicationListener<RedisSessionDestroyedEvent> {


    @Autowired
    private RedisService redisService;

    @Override
    public List<Object> getAllPrincipals() {
        Map<Object, List<SessionInformation>> principalSessions = getPrincipalSessions();
        return Arrays.asList(principalSessions.keySet().toArray());
    }

    @Override
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {

        Map<Object, List<SessionInformation>> principalSessions = getPrincipalSessions();

        List<SessionInformation> sessionInformations = principalSessions.get(principal);

        if (!CollectionUtils.isEmpty(sessionInformations)) {

            List<SessionInformation> siList = new ArrayList<>();

            for (SessionInformation sessionInformation : sessionInformations) {
                if (includeExpiredSessions || !sessionInformation.isExpired()) {
                    siList.add(sessionInformation);
                }
            }
            return siList;
        }

        return Collections.emptyList();
    }

    private Map<Object, List<SessionInformation>> getPrincipalSessions() {

        Set<String> keys = redisService.getKeys(RedisConstants.SECURITY_SESSION_KEY_PREFIX + "*");

        if (!CollectionUtils.isEmpty(keys)) {
            List<SessionInformation> sessionInformations = new ArrayList<>();
            for (String key : keys) {
                SessionInformation sessionInformation = redisService.get(key);
                sessionInformations.add(sessionInformation);
            }
            Map<Object, List<SessionInformation>> principals = sessionInformations.stream().collect(Collectors.groupingBy(SessionInformation::getPrincipal));
            return principals;
        }

        return Collections.emptyMap();
    }

    @Override
    public SessionInformation getSessionInformation(String sessionId) {
        return redisService.get(getKey(sessionId));
    }

    @Override
    public void refreshLastRequest(String sessionId) {
        SessionInformation sessionInformation = getSessionInformation(sessionId);
        sessionInformation.refreshLastRequest();
        redisService.set(getKey(sessionId), sessionInformation);
    }

    @Override
    public void registerNewSession(String sessionId, Object principal) {

        SessionInformation sessionInformation = new SessionInformation(principal, sessionId, new Date());
        redisService.set(getKey(sessionId), sessionInformation, RedisConstants.LOGIN_VALID_TIMING);

    }

    @Override
    public void removeSessionInformation(String sessionId) {
        redisService.removeKey(getKey(sessionId));
    }


    @Override
    public void onApplicationEvent(RedisSessionDestroyedEvent event) {
        redisService.removeKey(getKey(event.getSessionId()));
    }

    private String getKey(String sessionId) {
        return RedisConstants.SECURITY_SESSION_KEY_PREFIX + sessionId;
    }


}

4. SessionInformationMixin解决反序列化问题

package com.zzhua.blog.config.security.session;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Date;

/* 使用 mixin 解决 SessionInformation反序列化报错问题 */
public abstract class SessionInformationMixin  {

    @JsonCreator
    public SessionInformationMixin(@JsonProperty("principal") Object principal,
                                   @JsonProperty("sessionId") String sessionId,
                                   @JsonProperty("lastRequest") Date lastRequest) {
    }
}

5. TokenSessionAuthenticationStrategy

package com.zzhua.blog.config.security.session;

import com.zzhua.blog.config.ex.BizException;
import com.zzhua.blog.config.redis.RedisConstants;
import com.zzhua.blog.config.redis.RedisService;
import com.zzhua.blog.util.JsonUtil;
import com.zzhua.blog.util.Result;
import com.zzhua.blog.util.ResultCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;

// ConcurrentSessionControlAuthenticationStrategy & RegisterSessionAuthenticationStrategy
@Slf4j
@Component
public class TokenSessionAuthenticationStrategy implements SessionAuthenticationStrategy {

    @Value("${token.maxiumSessions:2}")
    private int maxiumSessions;

    @Value("${token.maxiumSessionsPreventsLogin:false}")
    private boolean maxiumSessionsPreventsLogin;

    @Autowired
    private RedisService redisService;

    @Autowired
    private RedisSessionRegistry redisSessionRegistry;

    @Override
    public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) throws SessionAuthenticationException {

        // 校验用户的会话数量
        validateMaximumSessions(authentication, response);

        // 如果会话数量没问题, 就注册新的会话
        registerNewSession(authentication, response);
    }


    private void validateMaximumSessions(Authentication authentication,HttpServletResponse response) {
        List<SessionInformation> princinpalSessions = redisSessionRegistry.getAllSessions(authentication.getPrincipal(), false);
        int sessionCount = princinpalSessions.size();

        int maxiumSessions = getMaximumSessionsForThisUser(authentication);
        if (sessionCount < maxiumSessions) {
            return;
        }
        if (maxiumSessions == -1) {
            return;
        }

        if (maxiumSessionsPreventsLogin) {
            log.error("会话数量超过限制: {}", JsonUtil.obj2Json(authentication.getPrincipal()));
            throw BizException.MAXIMUM_SESSION_EXCEED_ERR;
        }

        // 按最近一次访问时间升序
        princinpalSessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
        int maximumSessionsExceededBy = princinpalSessions.size() - maxiumSessions + 1;
        List<SessionInformation> sessionsToBeExpired = princinpalSessions.subList(0, maximumSessionsExceededBy);

        // 让超过数量的会话打上失效标记
        for (SessionInformation session : sessionsToBeExpired) {
            session.expireNow();
            redisService.set(RedisConstants.SECURITY_SESSION_KEY_PREFIX + session.getSessionId(), session);
        }

    }

    private void registerNewSession(Authentication authentication, HttpServletResponse response) {
        String token = UUID.randomUUID().toString().replace("-", "");

        redisSessionRegistry.registerNewSession(token, authentication.getPrincipal());

        JsonUtil.writeJsonToResponse(Result.ok(token), response);
    }

    private int getMaximumSessionsForThisUser(Authentication authentication) {
        // 此处, 也可以改成从数据库中查询
        return maxiumSessions;
    }

}

6. RedisSessionSecurityContextRepository

package com.zzhua.blog.config.security.session;

import com.zzhua.blog.config.redis.RedisConstants;
import com.zzhua.blog.config.redis.RedisService;
import com.zzhua.blog.config.security.UserDetailDTO;
import com.zzhua.blog.config.security.session.RedisSessionRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class RedisSessionSecurityContextRepository implements SecurityContextRepository {

    private static final String AUTHENTICATION_IN_SECURITY_CONTEXT_KEY = "AUTHENTICATION_IN_REDIS_SECURITY_CONTEXT";

    @Autowired
    private RedisService redisService;

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {

        SecurityContext securityContext = SecurityContextHolder.createEmptyContext();

        HttpServletRequest request = requestResponseHolder.getRequest();

        try {
            // 通过会话id 拿到 会话信息, 获取会话信息中的principal
            String authorization = request.getHeader(RedisConstants.AUTHORIZATIO_HEADER);
            if (authorization != null && redisService.existKey(getKey(authorization))) {
                SessionInformation sessionInformation = redisService.get(getKey(authorization));
                if (sessionInformation != null) {
                    UserDetailDTO userDetailDTO = (UserDetailDTO) sessionInformation.getPrincipal();
                    UsernamePasswordAuthenticationToken upAuthToken = new UsernamePasswordAuthenticationToken(userDetailDTO, "",userDetailDTO.getAuthorities());
                    securityContext.setAuthentication(upAuthToken);

                    // 将它保存到本地线程中, 避免同一次请求中, 多次从redis中反序列化
                    RequestedSessionInfomationHolder.set(sessionInformation);

                    // 给session续时间
                    if (!sessionInformation.isExpired()) {
                        redisService.expireKey(getKey(sessionInformation.getSessionId()), RedisConstants.LOGIN_VALID_TIMING);
                    }

                }
            }
        } catch (Exception e) {
            // do nothing...
        }

        request.setAttribute(AUTHENTICATION_IN_SECURITY_CONTEXT_KEY, securityContext.getAuthentication());

        return securityContext;
    }

    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {

        // 后面请求过程中, 有可能会对context中的Authentication对象作出修改 或者 刚开始没有但是后面又有了, 这里需要再次保存它
        // 需要结合 SecurityContextPersistenceFilter & HttpSessionSecurityContextRepository 一起看
        // (在HttpSessionSecurityContextRepository中有一个判断,那个判断是因为,原response被包装了,修改了flushBuffer方法,
        //   在修改前已经做了1次saveContext,在controller写出json时,就会flushBuffer。
        //   而如果只到了过滤器,还没到controller,那就需要过滤器里面做了,这就是为什么会有那个判断的原因。)
        Authentication authenticationBefore = (Authentication) request.getAttribute(AUTHENTICATION_IN_SECURITY_CONTEXT_KEY);
        Authentication authenticationAfter = context.getAuthentication();


        // 请求结束后, 认证对象没了, 如果会话还存在的话, 清除它
        if (authenticationAfter == null || AnonymousAuthenticationToken.class.isAssignableFrom(authenticationAfter.getClass())) {
            String authorizationHeader = request.getHeader(RedisConstants.AUTHORIZATIO_HEADER);

            if (authenticationBefore != null && redisService.existKey(getKey(authorizationHeader))) {
                redisService.removeKey(getKey(authorizationHeader));
            }
            return;
        }

        // Authentication与之前的不一样(之前必须存在), 更新它
        if (authenticationAfter != null && authenticationAfter != authenticationBefore) {
            String authorizationHeader = request.getHeader(RedisConstants.AUTHORIZATIO_HEADER);
            if (!StringUtils.isEmpty(authorizationHeader) && authenticationBefore != null) {
                redisService.set(getKey(authorizationHeader), authenticationAfter.getPrincipal());
            }
        }

    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
        String authorization = request.getHeader(RedisConstants.AUTHORIZATIO_HEADER);
        return !StringUtils.isEmpty(authorization) && redisService.existKey(getKey(authorization));
    }

    private String getKey(String sessionId) {
        return RedisConstants.SECURITY_SESSION_KEY_PREFIX + sessionId;
    }
}

7. RequestedSessionInfomationHolder

public class RequestedSessionInfomationHolder {

    private static final ThreadLocal<SessionInformation> SESSION_INFO_HOLDER = new ThreadLocal<>();

    public static SessionInformation get() {
        return SESSION_INFO_HOLDER.get();
    }

    public static void set(SessionInformation sessionInformation) {
        SESSION_INFO_HOLDER.set(sessionInformation);
    }

    public static void clear() {
        SESSION_INFO_HOLDER.remove();
    }

}

8. RedisConcurrentSessionFilter

package com.zzhua.blog.config.security.session;

import com.zzhua.blog.config.redis.RedisConstants;
import com.zzhua.blog.config.redis.RedisService;
import com.zzhua.blog.config.security.SysLogoutHandler;
import com.zzhua.blog.util.JsonUtil;
import com.zzhua.blog.util.Result;
import com.zzhua.blog.util.ResultCodeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/* 不能直接定义成bean, 否则会直接被Springboot给扫描到, 而添加到tomcat容器中 */
public class RedisConcurrentSessionFilter extends OncePerRequestFilter {

    @Autowired
    private RedisSessionRegistry sessionRegistry;

    @Autowired
    private RedisService redisService;

    @Autowired
    private SysLogoutHandler sysLogoutHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        SessionInformation info = RequestedSessionInfomationHolder.get();

        if (info != null) {

            // 如果会话失效了, 作退出
            if (info.isExpired()) {
                sysLogoutHandler.logout(request, response, SecurityContextHolder.getContext().getAuthentication());
                JsonUtil.writeJsonToResponse(Result.fail(ResultCodeEnum.SESSION_KEY_EXPIRED),response);
                return;
            }

            // 刷新会话最近一次访问时间
            this.sessionRegistry.refreshLastRequest(info.getSessionId());
        }

        filterChain.doFilter(request, response);

    }
}


UserDetailDTO

package com.zzhua.blog.config.security;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

@Data
public class UserDetailDTO implements UserDetails {

    private Integer userAuthId;

    private Integer userInfoId;

    private String nickname;

    private String avatar;

    private String bio;

    private String website;

    private Integer loginType;

    private String username;

    private Date loginTime;

    private List<String> perms;

    private List<String> roles;

    @JsonIgnore
    private String password;

    private Integer disabled;

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (!CollectionUtils.isEmpty(this.perms) || !CollectionUtils.isEmpty(this.roles)) {
            ArrayList<GrantedAuthority> list = new ArrayList<>();
            // 要加 ROLE_ 作为前缀的原因在 SecurityExpressionRoot#hasAnyAuthorityName(..)方法中
            List<GrantedAuthority> permAuthorities = roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
            List<GrantedAuthority> rolePermAuthories = AuthorityUtils.createAuthorityList(perms.toArray(new String[0]));
            list.addAll(permAuthorities);
            list.addAll(rolePermAuthories);
            return list;
        } else {
            return Collections.emptyList();
        }
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return disabled != null && disabled == 0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UserDetailDTO that = (UserDetailDTO) o;
        return Objects.equals(userInfoId, that.userInfoId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(userInfoId);
    }
}

配置security

1. SecurityConfig

package com.zzhua.blog.config.security;

import com.zzhua.blog.config.security.session.RedisConcurrentSessionFilter;
import com.zzhua.blog.config.security.session.RedisSessionRegistry;
import com.zzhua.blog.config.security.session.RedisSessionSecurityContextRepository;
import com.zzhua.blog.config.security.session.TokenSessionAuthenticationStrategy;
import com.zzhua.blog.util.ApplicationContextUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.web.cors.CorsConfiguration;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private RedisSessionSecurityContextRepository redisSecurityContextRepository;

    @Autowired
    private AuthenticationEntryPoint entryPoint;

    @Autowired
    private CorsConfiguration corsConfiguration;

    @Autowired
    private SysLogoutHandler sysLogoutHandler;

    @Autowired
    private SysLogoutSuccessHandler sysLogoutSuccessHandler;

    @Autowired
    private RedisSessionRegistry redisSessionRegistry;

    @Autowired
    private TokenSessionAuthenticationStrategy tokenSessionAuthenticationStrategy;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().matches("123456", "$2a$10$bwHJ2dlbsgf57X0NsJFtAucqs/cUeaSEc3Nox0VLl4Eo0O3KQMH9C"));
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/**/*.css")
                .antMatchers("/**/*.js")
                .antMatchers("/doc.html")
                .antMatchers("/webjars/**")
                .antMatchers("/**/*swagger*/**")
                .antMatchers("/v2/api-docs")
                .antMatchers("/favicon.ico")
                .antMatchers("/test/**")
                .antMatchers("/static/**");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http
                .csrf()
                    .disable() /* 关闭csrf, 否则会报错 */
                .cors()
                    .configurationSource(request -> corsConfiguration) /* 处理跨域 */
                .and()
                .securityContext()
                    .securityContextRepository(redisSecurityContextRepository)
                .and()
                .formLogin()
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .successHandler(authenticationSuccessHandler)
                    .failureHandler(authenticationFailureHandler) /* 让UsernamePasswordAuthenticationFilter处理登录 */
                .and()
                .logout()
                    .permitAll(true)
                    .logoutUrl("/logout")
                    .addLogoutHandler(sysLogoutHandler)
                    .logoutSuccessHandler(sysLogoutSuccessHandler) /* 退出登录 */
                .and()
                .authorizeRequests()
                    .antMatchers("/login/**").permitAll()
                    .anyRequest().authenticated() /* 须认证 */
                .and()
                .exceptionHandling()
                    .authenticationEntryPoint(entryPoint) /* 认证入口 */
                .and()
                .sessionManagement()
                    /* 自定义会话认证策略, 它最终被设置到AbstractAuthenticationProcessingFilter */
                    .withObjectPostProcessor(new ObjectPostProcessor<CompositeSessionAuthenticationStrategy>() {
                        @Override
                        public <O extends CompositeSessionAuthenticationStrategy> O postProcess(O object) {
                            List<SessionAuthenticationStrategy> list = Arrays.asList(tokenSessionAuthenticationStrategy);
                            CompositeSessionAuthenticationStrategy strategy = new CompositeSessionAuthenticationStrategy(list);
                            return (O) strategy;
                        }
                    })

        ;

        // 添加
        RedisConcurrentSessionFilter redisConcurrentSessionFilter = ApplicationContextUtil
                .getAppContext().getAutowireCapableBeanFactory().createBean(RedisConcurrentSessionFilter.class);
        http.addFilterBefore(redisConcurrentSessionFilter, SessionManagementFilter.class);

    }

}

SysUserDetailsService

package com.zzhua.blog.config.security;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zzhua.blog.config.ex.BizException;
import com.zzhua.blog.config.redis.RedisConstants;
import com.zzhua.blog.config.redis.RedisService;
import com.zzhua.blog.entity.UserAuthEntity;
import com.zzhua.blog.entity.UserInfoEntity;
import com.zzhua.blog.enums.CaptchaEnum;
import com.zzhua.blog.mapper.UserAuthMapper;
import com.zzhua.blog.mapper.UserInfoMapper;
import com.zzhua.blog.util.BeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

@Component
public class SysUserDetailsService implements UserDetailsService {

    @Autowired
    private UserAuthMapper userAuthMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Autowired
    private RedisService redisService;

    @Autowired
    private HttpServletRequest request;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 校验验证码
        /*String cid = request.getParameter("cid");
        String cCode = request.getParameter("cCode");
        if (StringUtils.isEmpty(cid) || StringUtils.isEmpty(cCode)) {
            throw BizException.CAPTCHA_CODE_ERR;
        }
        String captchaKey = RedisConstants.CAPTCHA_KEY_PATTERN
                .replaceFirst("\\{cid}", cid)
                .replaceFirst("\\{cType}", String.valueOf(CaptchaEnum.FOR_LOGIN.captchaType()));
        if (!Objects.equals(redisService.get(captchaKey), cCode)) {
            throw BizException.CAPTCHA_CODE_ERR;
        }
        redisService.removeKey(captchaKey);*/

        // 校验用户
        UserAuthEntity userAuthEntity = userAuthMapper.selectOne(new QueryWrapper<UserAuthEntity>()
                .lambda()
                .eq(UserAuthEntity::getUsername, username)
        );

        if (userAuthEntity == null) {
            throw BizException.USERNAME_OR_PWD_ERR;
        }

        // 用户信息
        UserInfoEntity userInfoEntity = userInfoMapper.selectOne(new QueryWrapper<UserInfoEntity>()
                .lambda()
                .eq(UserInfoEntity::getId, userAuthEntity.getUserInfoId())
        );

        // 用户信息 和 加载用户权限
        UserDetailDTO userDetailDTO = new UserDetailDTO();

        BeanUtil.copyBeanProps(userAuthEntity, userDetailDTO);
        BeanUtil.copyBeanProps(userInfoEntity, userDetailDTO);

        userDetailDTO.setUserInfoId(userInfoEntity.getId());
        userDetailDTO.setUserAuthId(userAuthEntity.getId());
        userDetailDTO.setLoginType(LoginTypeEnum.EMAIL_LOGIN.loginType());
        userDetailDTO.setPassword(userAuthEntity.getPassword());
        userDetailDTO.setLoginTime(new Date());

        List<String> permList =  userInfoMapper.listPermsForUser(userInfoEntity.getId());
        userDetailDTO.setPerms(permList.stream().filter(e -> !StringUtils.isEmpty(e)).collect(Collectors.toList()));

        return userDetailDTO;
    }


}

2. CorsConfig

package com.zzhua.blog.config.security;

import com.zzhua.blog.config.redis.RedisConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;

import java.util.Arrays;

@Configuration
public class CorsConfig {

    /* 跨域配置 */
    @Bean
    public CorsConfiguration corsConfiguration() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 预检请求能够被客户端缓存多久
        corsConfiguration.setMaxAge(3600L);
        // 是否允许客户端携带凭证,会设置到Access-Control-Allow-Credentials跨域响应头中
        corsConfiguration.setAllowCredentials(true);
        // 允许的域,会设置到Access-Control-Allow-Origin跨域响应头中
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        // 允许的请求方式,可以参考DefaultCorsProcessor和CorsConfiguration
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        // 允许浏览器请求携带的请求头
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        // 允许浏览器获取的响应头(这个不能用*)
        corsConfiguration.setExposedHeaders(Arrays.asList(RedisConstants.AUTHORIZATIO_HEADER));
        return corsConfiguration;
    }

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/468824.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【HCIP】IPV6综合实验(ripng,ospfv3,bgp,6to4)

目录 一、IP规划 二、 连通公网部分(IPv4) 三、R1、R2上IPv4&#xff0b;v6配置 四、IPV6部分ip配置 五、IPV6部分OSPF&BGP协议配置 Ⅰ、 ospf Ⅱ、bgp 六、联通网络 需求&#xff1a; 1、AR1处于IPV4&#xff0c;也有IPV6的地址&#xff0c;有两个环回 2、AR45678处…

Golang Gin 请求参数绑定与多数据格式处理

之前学习了使用Gin框架的Engine的默认路由功能解析HTTP请求。现在我们来学习gin框架的参数绑定操作和请求结果返回格式。 处理POST请求时&#xff0c;使用context.PostForm或者context.DefaultPostForm获取客户端表单提交的数据。 像上述这种只有username和password两个字段的表…

CS:APP 第7章链接分步编译(cpp/cc1/as/ld)遇到的问题

环境 WSL Ubuntu 22.04.2 LTS gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 问题 问题一 cc1 命令找不到 cc1 命令在 /usr/lib/gcc/x86_64-linux-gnu/11/cc1 里&#xff0c;注意不同操作系统等可能 cc1 的位置不一样&#xff0c;可以使用 find 或者 locate 命令搜索。 通过下…

聊点技术 | 架构瘦身,让Bonree ONE跑得更轻

4月21日&#xff0c;博睿数据ONE有引力2023春季产品发布会圆满落幕&#xff0c;一体化智能可观测平台Bonree ONE 2023春季正式版正式发布&#xff0c;这一次发布的版本更轻、更强、更智能。 Bonree ONE在上一版基础上削减50%组件数量&#xff0c;下架两大高耗能组件&#xff0c…

97-TCP为什么要有一个“TIME_WAIT“的状态

文章目录 1.TCP为什么要有一个"TIME_WAIT"的状态(1) 可靠的终止 TCP 连接。(2) 保证让迟来的 TCP 报文有足够的时间被识别并被丢弃 ; 2.拓展带外数据 1.TCP为什么要有一个"TIME_WAIT"的状态 "TIME_WAIT"状态存在的原因主要有两点: (1) 可靠的终…

成功经验分享,Nacos注册中心实践,带你玩转Nacos

1、什么是 Nacos &#xff1f; 官方&#xff1a;一个更易于构建云原生应用的动态服务发现(Nacos Discovery )、服务配置(Nacos Config)和服务管理平台。 集&#xff1a; 注册中心配置中心服务管理 平台 nacos的特性包括&#xff1a; 服务发现和服务健康监测动态配置服务动态…

行人检测(人体检测)4:C++实现人体检测(含源码,可实时人体检测)

行人检测(人体检测)4&#xff1a;C实现人体检测(含源码&#xff0c;可实时人体检测) 目录 行人检测(人体检测)4&#xff1a;C实现人体检测(含源码&#xff0c;可实时人体检测) 1. 前言 2. 行人检测(人体检测)检测模型&#xff08;YOLOv5&#xff09; &#xff08;1&#xf…

谈「效」风生 | 如何找到现有研发体系的「内耗问题」?

#第3期&#xff1a;如何找到现有研发体系的「内耗问题」&#xff1f;# 在上一期《谈到提升效能&#xff0c;我们应该如何下手&#xff1f;》我们聊到开始做研发效能的四个要点&#xff1a;评估现有流程、引入自动化工具、建立度量指标、持续改进。本期就围绕「评估现有研发体系…

二次创业接地气、强内功,三只松鼠从一棵树出发重造“人设”

民以食为天&#xff0c;自古以来&#xff0c;“吃”都是一门浅显与深奥并存的生意。产业链看似简单&#xff1a;种、收、制、卖&#xff0c;却足以令众多企业为之前赴后继十年、百年。 三只松鼠&#xff0c;正在这条变革的道路上砥砺前行。自去年4月开启全面转型以来&#xff…

C++ 赋值运算符重载

赋值运算符重载 运算符重载&#xff1a; C为了增强代码的可读性&#xff0c;可以对 运算符 进行重载&#xff0c;运算符重载 就是具有特殊函数名的函数&#xff0c;这个函数也具有返回值类型&#xff0c;函数名字和参数列表&#xff0c;它的返回值和参数列表的形式和普通函数…

基于SAM的二次开发案例收集分享

一、AnyLabeling[1]——制作人&#xff1a;vietanhdev AnyLabeling LabelImg Labelme Improved UI Autolabeling AnyLabeling软件是一个集成了YOLO、Segment Anything模型&#xff08;AI支持&#xff09;的高效数据标注工具&#xff0c;它可以通过点击目标的方式完成目标检…

商业银行财富管理“智能原生”能力呈阶梯化,AI助力商业模式趋向多元化发展

易观&#xff1a;金融业的财富管理从经营角度来看&#xff0c;是“客户与渠道管理场景运营产品研发”三位一体共同构建以客户为中心&#xff0c;数据驱动的业务经营体系。其中&#xff0c;“客户与渠道管理”是将客户利益作为核心目标&#xff0c;通过升级用户体验、客户全生命…

获奖名单公布|香港BlockBooster x Moonbeam黑客松圆满收官

Moonbeam基金会赞助的”Into the Socialverse”主题的BlockBooster黑客松于近日落幕。该活动由BlockBooster、OKX、Gitcoin和OxU香港区块链俱乐部联合主办&#xff0c;共有22个开发团队参赛。经过多位评委的严格筛选&#xff0c;3支优秀团队脱颖而出&#xff0c;获得Moonbeam基…

zookeeper集群命令使用

1.zookeeper脚本使用(地址填写集群中任意一个主机地址) 连接客户端命令行 /etc/zookeeper/zookeeper/bin/zkCli.sh -server 10.1.60.112:2181 启动zookeeper服务 /etc/zookeeper/zookeeper/bin/zkServer.sh start 停止zookeeper服务 /etc/zookeeper/zookeeper/bin/zkServer…

春风吹,战鼓擂,忆享科技-云服务事业部春季员工关怀活动集锦,温情相伴

前言 时序更替&#xff0c;忆享科技又迎来新的一年。回顾2022&#xff0c;忆享科技在风雨中前行&#xff0c;实现了一次又一次的突破。在这2023年春暖花开&#xff0c;万物复苏的美好季节&#xff0c;忆享科技怀抱着它满满的关怀向大家走来&#xff01;春季云服务事业部开展了五…

推动科技企业成长,开源网安受邀参加数字经济企业孵化器建设座谈会

近日&#xff0c;为更好地做好数字经济孵化器的孵化培育工作&#xff0c;推动数字经济孵化器和入驻企业高质量发展&#xff0c;高创公司召开数字经济企业孵化器建设座谈会。高新区工委委员、管委会副主任贺菲出席会议&#xff0c;开源网安合肥公司总经理菅志刚受邀参加本次座谈…

vue生命周期代码示范--Vue基本介绍--MVVM-示意图--数据渲染--事件绑定--修饰符--组件化--和全部代码示范

目录 Vue 基本介绍 官网 git 地址: MVVM-示意图 解读 MVVM 思想(上图) 下载官网 简单的代码示例方便理解 Vue 数据绑定机制分析! 注意事项和使用细节 数据单向渲染 基本说明 应用实例 注意事项和使用细节 数据双向绑定 应用实例 ​编辑代码实现 代码综合-单…

带头双向循环链表--数据结构

魔王的介绍&#xff1a;&#x1f636;‍&#x1f32b;️一名双非本科大一小白。魔王的目标&#xff1a;&#x1f92f;努力赶上周围卷王的脚步。魔王的主页&#xff1a;&#x1f525;&#x1f525;&#x1f525;大魔王.&#x1f525;&#x1f525;&#x1f525; ❤️‍&#x1f…

Rocket 框架基础

Rocket v0.5 DOC Rocket是Rust的一个web框架&#xff0c;它使编写快速、安全的web应用程序变得简单&#xff0c;而不会牺牲灵活性、可用性或类型安全性。 类型安全 从请求到响应&#xff0c;Rocket确保您的类型有意义。样板免费 把时间花在编写真正重要的代码上&#xff0c;让…

Amazon S3 对象存储Java API操作记录(Minio与S3 SDK两种实现)

缘起 今年(2023年) 2月的时候做了个适配Amazon S3对象存储接口的需求&#xff0c;由于4月份自学考试临近&#xff0c;一直在备考就拖着没总结记录下&#xff0c;开发联调过程中也出现过一些奇葩的问题&#xff0c;最近人刚从考试缓过来顺手记录一下。 S3对象存储的基本概念 …