Spring Security

一.简单案例 原始页面 对controller起保护作用

原始页面 使用默认用户名和密码

(1)引入jar包

<!-- 引入spring-security 的starter依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.3.12.RELEASE</version>
</dependency>

(2)登录用户名:user 控制台中有密码

SecurityConfig总配置
package com.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.entity.SysMenu;
import com.mapper.SysMenuMapper;
import com.service.DynamicSecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//使用注解对权限进行控制
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /*//redis
    @Autowired
    private RedisSecurityContextRepository redisSecurityContextRepository;*/
    //jwt
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private SysMenuMapper sysMenuMapper;
    //创建 DynamicSecurityService 实现
    @Bean
    public DynamicSecurityService securityService(){
        return new DynamicSecurityService(){
            @Override
            public Map<String, ConfigAttribute> loadDataSource() {
                QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
                wrapper.isNotNull("perms")
                        .gt("length(perms)","0")
                        .select("url","perms");
                List<SysMenu> sysMenus = sysMenuMapper.selectList(wrapper);

                Map<String,ConfigAttribute> map = new HashMap<>();
                for (SysMenu sysMenu : sysMenus) {
                    map.put(sysMenu.getUrl(),new org.springframework.security.access.SecurityConfig(sysMenu.getPerms()));
                }

                return map;
            }
        };
    }
    //    创建自定义读取数据库配置
    @Bean
    public DynamicSecurityMetadataSource dynamicSecurityMetadataSource(){
        return new DynamicSecurityMetadataSource();
    }
    // 创建选举器对象
    @Bean
    public AccessDecisionVoter dynamicVoter(){
        return new DynamicSecurityVoter();
    }
    //AccessDecisionManager  使用自定义的选举器
    @Bean
    public AccessDecisionManager dynamicSecurityAccessDecisionManager(){
        return new AffirmativeBased(Arrays.asList(dynamicVoter()));
    }
    //创建自定义过滤器
    @Bean
    public DynamicSecurityFilter dynamicSecurityFilter(){
        return new DynamicSecurityFilter();
    }

    //对白名单路径放行
    @Bean
    public IgnoreUrlsProperties ignoreUrlsProperties() {
        return new IgnoreUrlsProperties();
    }

    //在controller中模拟Filter 使用AuthenticationManager完成认证
    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    //替换默认的密码格式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //将自定义的过滤器添加到过滤链中
        http.addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
        http.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //  添加过滤器到指定过滤器之后
        //http.addFilterBefore(new MyFilter(), WebAsyncManagerIntegrationFilter.class);
        // 添加过滤器到指定过滤器之前
        //http.addFilterAfter(new MyFilter(), UsernamePasswordAuthenticationFilter.class);

        //super.configure(http);    自己设置密码时,要注销

        //对UsernamePasswordAuthenticationFilter 进行定制 处理登录认证
        /*http.formLogin()
        .loginPage("/login.html") //指定登录页
        .loginProcessingUrl("/login") //只要指定登录页 就必须提供路径 并且和表单一致
        .usernameParameter("username") //指定表单中提供的用户名 key
        .passwordParameter("password") //指定表单中提供的密码 key
        .defaultSuccessUrl("/main.html")//认证成功:重定向到主页面
        .failureUrl("/register.html")//认证成功:重定向到注册页面
        //...
        ;*/

        //对LogoutFilter 进行定制  处理登出操作
        http.logout()
                .logoutUrl("/logout")
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                    //前后端分离
                    httpServletResponse.setContentType("application/json");
                    httpServletResponse.setCharacterEncoding("utf-8");
                    httpServletResponse.getWriter().println("登出成功");
                }
            })
        //...
        ;

        //对ExceptionTranslationFilter 进行定制 处理认证和鉴权失败
        http.exceptionHandling()
        .authenticationEntryPoint(new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                //前后端未分离
                //httpServletResponse.sendRedirect("/login.html");//认证失败后返回登录页面
                //前后端分离
                httpServletResponse.setContentType("application/json");
                httpServletResponse.setCharacterEncoding("utf-8");
                httpServletResponse.getWriter().println("你还没有登录");
            }
        })
        .accessDeniedHandler(new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json");
                httpServletResponse.setCharacterEncoding("utf-8");
                httpServletResponse.getWriter().println("你没有权限访问");
            }
        })
        //...
        ;

        //对FilterSecurityInterceptor 进行定制  认证和鉴权
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry urlRegistry = http.authorizeRequests();
        //对白名单进行遍历
        for (String url :ignoreUrlsProperties().getUrls()){
            urlRegistry.mvcMatchers(url).permitAll();
        }
        //设置权限
        //urlRegistry.mvcMatchers("/hello").hasAnyAuthority("aa");

        urlRegistry.anyRequest().authenticated() //所有请求都拦截 必须认证
        //...
        ;

        //对CsrfFilter 进行定制 跨域请求伪造攻击的防护
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);; //关闭
        /*//设置自定义的SecurityContext持久化机制
        http.securityContext()
                .securityContextRepository(redisSecurityContextRepository);*/
    }
}
连接数据库

Service实现UserDetailsService接口

public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private SysUserMapper userMapper;
    @Autowired
    private SysMenuMapper menuMapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_name", s);
        SysUser sysUser = userMapper.selectOne(queryWrapper);
        if (sysUser==null){
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        //设置权限操作
        List<String> perms = menuMapper.selectPermsByUserId(sysUser.getId());
        List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(perms.toArray(new String[]{}));
        return  new User(sysUser.getUsername(), sysUser.getPassword(),authorityList);
    }
}
对密码进行加密设置
 //替换默认的密码格式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

二.Spring Security的认证流程

三.自定义登录接口

controller中的编写
//认证前进行封装 UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
            //使用 AuthenticationManager 与数据库进行对比
            Authentication authenticate = authenticationManager.authenticate(authentication);
            //保存认证后的用户信息
            SecurityContextHolder.getContext().setAuthentication(authenticate);
前端分离和未分离

未分离:接口返回为页面

分离:接口返回为json数据

四.白名单的处理

application中的配置
#配置白名单 放行路径
secure.ignored.urls=/放行路径
自定义Properties类读取配置
@Getter
@Setter
@ConfigurationProperties(prefix = "secure.ignored")
public class IgnoreUrlsProperties {
    private List<String> urls = new ArrayList<>();
}
SecurityConfig对白名单路径放行
@Bean
    public IgnoreUrlsProperties ignoreUrlsProperties() {
        return new IgnoreUrlsProperties();
    }
@Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置白名单
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
        //不需要保护的资源路径允许访问
        for (String url : ignoreUrlsProperties().getUrls()) {
            registry.antMatchers(url).permitAll();
        }
        ...
    }

五.Spring Security的授权

基于路径配置的权限控制
//对FilterSecurityInterceptor 进行定制
http.authorizeRequests()
    //允许匿名访问登录接口  ant表达式 ?匹配一个字符 *匹配任意个字符 **匹配任意个目录
    .antMatchers("/user/form/login").permitAll()
    .anyRequest().authenticated();所有请求都拦截 必须认证
基于方法注解的权限控制
1.@EnableGlobalMethodSecurity(prePostEnabled = true)//使用注解对权限进行控制
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

    }
2.在controller中使用注解
    @PreAuthorize("hasRole('admin')")
    public String hello(){
        return "";
    }

六.Spring Security动态权限控制

  1. 自定义过滤器拦截
package com.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;

public class DynamicSecurityFilter extends FilterSecurityInterceptor {
    @Autowired
    private IgnoreUrlsProperties ignoreUrlsProperties;

    //重写父类set方法注入属性
    @Override
    @Autowired
    public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
        super.setAccessDecisionManager(accessDecisionManager);
    }

    @Autowired
    public void setSecurityMetadataSource(DynamicSecurityMetadataSource newSource) {
        super.setSecurityMetadataSource(newSource);
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        //对白名单放行
        List<String> urls = ignoreUrlsProperties.getUrls();
        PathMatcher pathMatcher=new AntPathMatcher();//路径匹配器
        for (String url : urls) {
            if (pathMatcher.match(url,((HttpServletRequest)request).getRequestURI())) {
                chain.doFilter(request,response);
                return;
            }
        }
        // 针对跨域时 OPTIONS请求直接放行
        if (((HttpServletRequest) request).getMethod().equals(HttpMethod.OPTIONS)) {
            chain.doFilter(request,response);
            return;
        }
        super.doFilter(request, response, chain);
    }
}
  1. 读取数据库中的设置
package com.config;

import com.service.DynamicSecurityService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import javax.annotation.PostConstruct;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private DynamicSecurityService service;

    private Map<String, ConfigAttribute> configAttributeMap;

    @PostConstruct//在初始化后执行1次,从数据库中加载权限配置信息
    public void loadDataSource() {
        configAttributeMap = this.service.loadDataSource();
        //遍历数据
        /*configAttributeMap.forEach((k, v) -> {
            System.out.println(k + "-->" + v);
        });*/
    }
    //获取本次需要的权限
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //获取本次请求的uri路径
        String url = ((FilterInvocation) o).getRequestUrl();
        String path = null;
        try {
            path = new URI(url).getPath();
        } catch (URISyntaxException e) {
            e.printStackTrace();
            throw new IllegalArgumentException(e);
        }
        //遍历configAttributeMap,获取所需要的权限
        PathMatcher pathMatcher = new AntPathMatcher();
        List<ConfigAttribute> configAttributeList = new ArrayList<>();
        for (Map.Entry<String, ConfigAttribute> entry : configAttributeMap.entrySet()) {
            String key = entry.getKey();
            if (pathMatcher.match(key, path)) {
                configAttributeList.add(entry.getValue());
            }
        }
        return configAttributeList;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}
3.自定义DynamicSecurityVoter
package com.config;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.CollectionUtils;

import java.util.Collection;

public class DynamicSecurityVoter implements AccessDecisionVoter<Object> {
    @Override
    //判断ConfigAttribute是否可以使用当前Voter进行投票判断
    public boolean supports(ConfigAttribute attribute) {
        return attribute.getAttribute() != null; //需要权限 true
    }

    @Override
    //判断Filter中构建的安全对象是否为当前Voter支持
    public boolean supports(Class<?> clazz) {
        return true;
    }
    //authentication 用户登录后的认证对象 包含 用户名 密码 权限
    //Collection 代表当前路径下的权限列表
    //返回值 代表是否 通过 1 通过 -1 不通过 0 弃权
    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        //如果本次访问没有权限要求,则弃权
        if (CollectionUtils.isEmpty(attributes)) {
            return AccessDecisionVoter.ACCESS_ABSTAIN;
        }

        //如果用户没有任何权限,则拒绝
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        if (CollectionUtils.isEmpty(authorities)) {
            return AccessDecisionVoter.ACCESS_DENIED;
        }

        //遍历所需权限,判断当前用户是否拥有需要的权限
        for (ConfigAttribute needAttribute : attributes) {
            //遍历用户持有的权限
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().trim().equalsIgnoreCase(needAttribute.getAttribute().trim())) {
                    return AccessDecisionVoter.ACCESS_GRANTED;
                }
            }
        }
        //没有匹配的权限,则拒绝
        return AccessDecisionVoter.ACCESS_DENIED;
    }
}
4.通过SecurityConfig将三者联系起来
//创建 DynamicSecurityService 实现
    @Bean
    public DynamicSecurityService securityService(){
        return new DynamicSecurityService(){
            @Override
            public Map<String, ConfigAttribute> loadDataSource() {
                QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
                wrapper.isNotNull("perms")
                        .gt("length(perms)","0")
                        .select("url","perms");
                List<SysMenu> sysMenus = sysMenuMapper.selectList(wrapper);

                Map<String,ConfigAttribute> map = new HashMap<>();
                for (SysMenu sysMenu : sysMenus) {
                    map.put(sysMenu.getUrl(),new org.springframework.security.access.SecurityConfig(sysMenu.getPerms()));
                }

                return map;
            }
        };
    }
    //    创建自定义读取数据库配置
    @Bean
    public DynamicSecurityMetadataSource dynamicSecurityMetadataSource(){
        return new DynamicSecurityMetadataSource();
    }
    // 创建选举器对象
    @Bean
    public AccessDecisionVoter dynamicVoter(){
        return new DynamicSecurityVoter();
    }
    //AccessDecisionManager  使用自定义的选举器
    @Bean
    public AccessDecisionManager dynamicSecurityAccessDecisionManager(){
        return new AffirmativeBased(Arrays.asList(dynamicVoter()));
    }
    //创建自定义过滤器
    @Bean
    public DynamicSecurityFilter dynamicSecurityFilter(){
        return new DynamicSecurityFilter();
    }
//将过滤器添加到该方法中
protected void configure(HttpSecurity http) throws Exception {
        //将自定义的过滤器添加到过滤链中
        http.addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
}

七.认证信息的处理 通过session和cookie保存

原因:在分布式情况下 无法直接使用这种方式保存---相当于在不同客户端需要分别进行登录
1.使用redis解决
添加redis的starter依赖
<!-- 引入redis的starter依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.3.12.RELEASE</version>
</dependency>
application添加redis连接配置
#配置redis
spring.redis.host=192.168.136.128
spring.redis.port=6379
配置RedisTemplate
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
         // key 序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // value 序列化
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        // 注入连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置事务支持
        redisTemplate.setEnableTransactionSupport(true);
        // 让设置生效
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}
自定义SecurityContextRepository
package com.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.stereotype.Component;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Arrays;

@Component
public class RedisSecurityContextRepository implements SecurityContextRepository {
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
    public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";

    private String findAuthenticatedUuid(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        if(cookies == null || cookies.length == 0){
            return null;
        }

        return Arrays.stream(cookies).filter(c -> SPRING_SECURITY_CONTEXT_KEY.equalsIgnoreCase(c.getName())).findFirst().map(Cookie::getValue).orElse(null);

    }
    @Override
    //获取redis中的认证信息
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {

        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpServletResponse response = requestResponseHolder.getResponse();
        String uuid = findAuthenticatedUuid(request);
        if(uuid == null){
            return SecurityContextHolder.createEmptyContext();
        }
        //uuid有值
        Object context = redisTemplate.opsForValue().get(uuid);
        //判断是否到期 到期了为null
        if(context == null){
            return SecurityContextHolder.createEmptyContext();
        }
        response.setHeader("uuid",uuid);
        return (SecurityContext) context;
    }
    //存储认证信息 redis
    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {

        final Authentication authentication = context.getAuthentication();
        //判断认证信息是否为空                或者是匿名用户
        if (authentication == null || trustResolver.isAnonymous(authentication)) {
            //SecurityContext is empty or contents are anonymous - context will not be stored in Redis
            return;
        }
        //获取响应头的uuid
        String uuid = response.getHeader("uuid");
        if(uuid == null || uuid.isEmpty()){
            return;
        }
        //存储到redis中
        redisTemplate.opsForValue().set(uuid,context, Duration.ofMinutes(30));
    }

    @Override
    //判断本次请求能否获取登录信息
    public boolean containsContext(HttpServletRequest request) {
        //请求中有uuid 通过这个uuid从redis中得到认证信息
        Cookie[] cookies = request.getCookies();
        if(cookies == null || cookies.length==0){
            return false;
        }
        String uuid = null;
        for (Cookie cookie : cookies) {
            if( cookie.getName().equals(SPRING_SECURITY_CONTEXT_KEY) ){
                uuid = cookie.getValue();
            }
        }
        if (uuid==null){
            return false;
        }
        //说明client传递了uuid uuid有值
        Object context = redisTemplate.opsForValue() .get(uuid);
        if(context == null){
            return false;
        }
        return true;
    }
}
修改UserController
//认证前进行封装 UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
        //使用 AuthenticationManager 与数据库进行对比
        Authentication authenticate = authenticationManager.authenticate(authentication);
        //保存认证后的用户信息
        SecurityContextHolder.getContext().setAuthentication(authenticate);
        //生成用户登录的uuid,将其通过cookie响应回client
        String uuid = UUID.randomUUID().toString();
        response.setHeader("uuid",uuid);
        Cookie cookie = new Cookie(SPRING_SECURITY_CONTEXT_KEY,uuid);
        cookie.setPath("/");
SecurityConfig配置
@Autowired
    private RedisSecurityContextRepository redisSecurityContextRepository;
@Override
    protected void configure(HttpSecurity http) throws Exception { 
         //禁用csrf和session
         http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        
        //设置自定义的SecurityContext持久化机制
        http.securityContext()
                .securityContextRepository(redisSecurityContextRepository)
        
    }
2.使用jwt解决
原因:redis代替session存储信息 本质存储在后端 每次都要进行访问
jwt不是存储后端而是client jwt本质为一个加密的字符串
引入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
application配置密钥、过期时间以及使用的tokenHead
#配置JWT
#JWT存储的请求头
jwt.tokenHeader=Authorization
#JWT加解密使用的密钥
jwt.secret=bz-mall-admin-secret
#JWT的超期限时间(60*60*24*7)
jwt.expiration=604800
#JWT的标识前缀
jwt.tokenHead='Bearer '
引入工具类
package com.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * JwtToken生成的工具类
 * JWT token的格式:header.payload.signature
 * header的格式(算法、token的类型):
 * {"alg": "HS512","typ": "JWT"}
 * payload的格式(用户名、创建时间、生成时间):
 * {"sub":"wang","created":1489079981393,"exp":1489684781}
 * signature的生成算法:
 * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
 * Created by xushy on 2022年12月28日.
 */
@Component
public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String CLAIM_KEY_AUTHORITIES = "authorities";
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     * @param userDetails 从数据库中查询出来的用户信息
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 根据负责生成JWT的token
     */
    public String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从token中获取JWT中的负载
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            LOGGER.info("JWT格式验证失败:{}", token);
        }
        return claims;
    }

    /**
     * 生成token的过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 从token中获取登录用户名
     */
    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }


    /**
     * 验证token是否还有效
     *
     * @param token       客户端传入的token
     */
    public boolean validateToken(String token) {
        return getClaimsFromToken(token) != null && !isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     */
    private boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    /**
     * 从token中获取过期时间
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 根据用户信息生成token
     */
    public String generateToken(Authentication authentication) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, authentication.getName());
        claims.put(CLAIM_KEY_CREATED, new Date());
        claims.put(CLAIM_KEY_AUTHORITIES,authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        return generateToken(claims);
    }

    /**
     * 当原来的token没过期时是可以刷新的
     *
     * @param oldToken 带tokenHead的token
     */
    public String refreshHeadToken(String oldToken) {
        if(StringUtils.isEmpty(oldToken)){
            return null;
        }
        String token = oldToken.substring(tokenHead.length());
        if(StringUtils.isEmpty(token)){
            return null;
        }
        //token校验不通过
        Claims claims = getClaimsFromToken(token);
        if(claims==null){
            return null;
        }
        //如果token已经过期,不支持刷新
        if(isTokenExpired(token)){
            return null;
        }
        //如果token在30分钟之内刚刷新过,返回原token
        if(tokenRefreshJustBefore(token,30*60)){
            return token;
        }else{
            claims.put(CLAIM_KEY_CREATED, new Date());
            return generateToken(claims);
        }
    }

    /**
     * 判断token在指定时间内是否刚刚刷新过
     * @param token 原token
     * @param time 指定时间(秒)
     */
    private boolean tokenRefreshJustBefore(String token, int time) {
        Claims claims = getClaimsFromToken(token);
        Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
        Date refreshDate = new Date();
        //刷新时间在创建时间的指定时间内
        if(refreshDate.after(created)&&refreshDate.before(new Date(created.getTime() + time*1000))){
            return true;
        }
        return false;
    }

    public Authentication getAuthentication(String authToken) {
        Claims claims = getClaimsFromToken(authToken);
        String username = claims.getSubject();
        List<String> authorities = claims.get(CLAIM_KEY_AUTHORITIES, List.class);

        return new UsernamePasswordAuthenticationToken(username,"N/A",authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
    }
}
自定义JwtAuthenticationTokenFilter
package com.config;

import com.util.JwtTokenUtil;
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.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
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;

/**
 * JWT登录授权过滤器
 * Created by xushy on 2022年12月28日
 */
@Component
//一次请求只会执行该类一次
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);
        //判断 authHeader 是否为空 并且格式是否正确
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer " 去除前缀
            //String username = jwtTokenUtil.getUserNameFromToken(authToken);
                                    //判断是不是程序颁发的
            if (jwtTokenUtil.validateToken(authToken)) {
                //从token中解析回认证信息,用于后续的授权管理
                Authentication authentication = jwtTokenUtil.getAuthentication(authToken);
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        //放行请求
        chain.doFilter(request, response);
    }
}
SecurityConfig配置
@Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
protected void configure(HttpSecurity http) throws Exception{
    ...
    http.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); 
    ...
}
修改UserController
//认证前进行封装 UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
            //使用 AuthenticationManager 与数据库进行对比
            Authentication authenticate = authenticationManager.authenticate(authentication);
            //保存认证后的用户信息
            SecurityContextHolder.getContext().setAuthentication(authenticate);
            //将用户认证信息转换为jwt,通过响应头传递给客户端
            String token = jwtTokenUtil.generateToken(authenticate);
            resp.setHeader(tokenHeader,tokenHead+token);
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值