Spring Security 自定义权限实现 redis token 登陆验证

Posted by 浮生 on 02-16,2020

前言

  本文的实现方式主要针于与对spring security有所了解的读者,通过增加filter的方式,替换代替spring security自带的的能登陆逻辑。

准备工作

pom 配置

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

   <dependencies>
        <!-- spring security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- 链接mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.9.0</version>
        </dependency>
    </dependencies>

其他的pom配置这里不在贴出,有需要的可自行添加。

redis 配置

  RedisTemplate要来读取缓存在redis里面的token,登陆的时候需要使用到。这里为了以后扩展方便,使用RedisStandaloneConfiguration的方式配置RedisTemplate,可以自由配置端口和数据库。

package com.auth.frame.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * @ClassName SecurityRedisConfig
 * @Author WFS
 * @Date 2020/1/21 15:56
 */
@Configuration
public class SecurityRedisConfig {

    @Bean("sysTokenRedisTemplate")
    public RedisTemplate<String, String> getTokenRedisTemplate() {
        return buildRedisTemplateByDataBase(10);
    }

    private RedisTemplate<String, String> buildRedisTemplateByDataBase(Integer dataBase) {
        RedisTemplate<String, String> template = new StringRedisTemplate();
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName("localhost");
        redisStandaloneConfiguration.setPort(6379);
        redisStandaloneConfiguration.setDatabase(dataBase);
        JedisClientConfiguration.JedisClientConfigurationBuilder clientConfiguration = JedisClientConfiguration.builder();
        clientConfiguration.connectTimeout(Duration.ofSeconds(60));
        JedisConnectionFactory codeFactory = new JedisConnectionFactory(redisStandaloneConfiguration,
                clientConfiguration.build());
        template.setConnectionFactory(codeFactory);
        template.setValueSerializer(new StringRedisSerializer());
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

跨域配置

  一般使用token的都是前后端分离,跨域这个问题总是要解决。springboot框架自带跨域的filter的配置,但是那个配置有些时候老是会有问题,我建议自己配置filter的方式来增加跨域的支持。

package com.auth.frame.security.processor;

import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @ClassName CROSFilter
 * @Author WFS
 * @Date 2020/2/7/0007 16:58
 */
public class CORSFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String origin = request.getHeader("Origin");
        response.setHeader("Access-Control-Allow-Origin", origin);
        response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.addHeader("Access-Control-Allow-Headers",
                "Content-Type, Authorization");
        response.addHeader("Access-Control-Allow-Credentials",
                "true");
        if (RequestMethod.OPTIONS.toString().equals(request.getMethod())) {
            servletResponse.getWriter().println("ok");
            return;
        }
        filterChain.doFilter(request ,response);
    }

    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void destroy() {

    }
}

注册Filter

package com.auth.frame.security.config;

import com.auth.frame.security.processor.CORSFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName FilterConfig
 * @Author WFS
 * @Date 2020/2/7/0007 16:57
 */
@Configuration
public class FilterConfig {
    @Bean(name = "CORSFilter")
    public FilterRegistrationBean<CORSFilter> setFilter() {
        FilterRegistrationBean<CORSFilter> filterBean = new FilterRegistrationBean<>();
        filterBean.setFilter(new CORSFilter());
        filterBean.setOrder(Integer.MIN_VALUE);
        filterBean.setName("CORSFilter");
        filterBean.addUrlPatterns("/*");
        return filterBean;
    }
}

  1. 其余的选项可以按照自己的需求配置filter
  2. filterBean.setOrder(Integer.MIN_VALUE); 保证了filter 第一个执行。

原理

  1. token 登陆

  spring security是由一大批filter组成,其中UsernamePasswordAuthenticationFilter是用户登陆验证时候的filter,我们要做的就是登陆验证之前检验token,如果token符合我们规则,那么从token里面取出授权信息,然后在SecurityContextHolder里面增加授权信息。这样,后面经过UsernamePasswordAuthenticationFilter时就不会使用自带的登陆验证逻辑检验。如果token不符合规则,直接滤过,UsernamePasswordAuthenticationFilter检验没有授权信息,会引导到登陆页面。

  1. 自定义用户权限

  自定义用户权限主要用户来配合token做权限判断,从token里面取出来的授权信息就是关联我们的自定义权限。要实现自定义权限我们需要实现三个接口:UserDetailsService,该接口主要获取用户对应的权限;FilterInvocationSecurityMetadataSource启动项目时候所有url的权限信息;AccessDecisionManager投票器,根据用户的权限信息和url权限信息来实现允许或者拒绝的逻辑。

用户信息表

  1. 用户表

package com.auth.frame.security.bean;


import com.auth.frame.common.BaseEntity;
import lombok.*;

import javax.persistence.*;
import java.util.Set;


@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_sys_user", indexes = {@Index(columnList = "id"), @Index(columnList = "phoneNum")})
public class User extends BaseEntity {
    @Column(unique = true)
    private String phoneNum;
    @Column(unique = true)
    private String userName;
    @Column
    private String salt;
    @Column
    private String passWord;
    @Column
    private Boolean accountNonExpired;
    @Column
    private Boolean accountNonLocked;
    @Column
    private Boolean credentialsNonExpired;
    @Column
    private Boolean enabled;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "t_sys_user_role", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}
            , inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
    private Set<Role> roles;

}
  1. 角色表

package com.auth.frame.security.bean;

import com.auth.frame.common.BaseEntity;
import lombok.*;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;

@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_sys_role", indexes = {@Index(columnList = "id")})
public class Role extends BaseEntity {
    @Column
    private String name;
    @Column
    private String auth;
}
  1. URL表

package com.auth.frame.security.bean;


import com.auth.frame.common.BaseEntity;
import lombok.*;

import javax.persistence.*;
import java.util.Set;


@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_sys_url", indexes = {@Index(columnList = "id")})
public class URL extends BaseEntity {
    @Column
    private String name;
    @Column
    private String path;

    @ManyToMany (fetch = FetchType.EAGER)
    @JoinTable(name = "t_sys_url_role", joinColumns = {@JoinColumn(name = "url_id", referencedColumnName = "id")}
            , inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
    private Set<Role> roles;
}
  1. BaseEntity 主要是提供主键和创建、修改时间的字段。
  2. 注解中使用了 lombok。
  3. 角色、用户和URL使用的是单边对应,如有需要可改为多边对应。

自定义认证

  1. UserDetails

  UserDetails是spring security中的用户信息接口,通过实现UserDetails可以和User表关联起来。

package com.auth.frame.security.processor;

import com.auth.frame.security.bean.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @ClassName CSTUserDetailService
 * @Author WFS
 * @Date 2020/1/21 10:39
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
public class CSTUserDetails implements UserDetails {
    private Collection<SimpleGrantedAuthority> authorities;
    private String username;
    private String userId;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private Boolean enabled;

    public CSTUserDetails(Collection<SimpleGrantedAuthority> authorities, String username, String userId, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) {
        this.authorities = authorities;
        this.username = username;
        this.userId = userId;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.enabled = enabled;
    }

    public CSTUserDetails(User user) {
        this.authorities = CSTUserDetailsService.getAuthorities(user);
        this.username = user.getUserName();
        this.userId = user.getId();
        this.accountNonExpired = user.getAccountNonExpired();
        this.accountNonLocked = user.getAccountNonLocked();
        this.credentialsNonExpired = user.getCredentialsNonExpired();
        this.enabled = user.getEnabled();
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return null;
    }

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

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

authorities是用户对应的权限,其实就是一个字符串列表,可以自行拓展。

  1. UserDetailsService

  UserDetailsService接口规定了通过用户名获取用户信息的接口,即获取UserDetails。

package com.auth.frame.security.processor;

import com.auth.frame.security.bean.Role;
import com.auth.frame.security.bean.User;
import com.auth.frame.security.service.UserService;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * @ClassName CSTUserDetailsService
 * @Author WFS
 * @Date 2020/1/21 10:50
 */
@Component
public class CSTUserDetailsService implements UserDetailsService {


    private final UserService userService;

    public CSTUserDetailsService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userService.getCacheUser(s);
        if (user == null) {
            throw new UsernameNotFoundException("User Not Find!");
        } else {
            Collection<SimpleGrantedAuthority> authorities = getAuthorities(user);
            return new CSTUserDetails(authorities, user.getUserName(), user.getId(), user.getAccountNonExpired(), user.getAccountNonLocked(), user.getCredentialsNonExpired(), user.getEnabled());
        }
    }

    public static Collection<SimpleGrantedAuthority> getAuthorities(User user) {
        Set<Role> roleSet = user.getRoles();
        Collection<SimpleGrantedAuthority> authorities = new HashSet<>();
        if (roleSet != null && !roleSet.isEmpty()) {
            roleSet.forEach(role -> authorities.add(new SimpleGrantedAuthority(role.getAuth())));
        }
        return authorities;
    }
}

loadUserByUsername函数中的参数s即为用户名,userService.getCacheUser的作用是根据用户名获取用户,此处使用的自己写的从缓存获取用户名,可以自由发挥,只要能构建UserDetails即可。

  1. FilterInvocationSecurityMetadataSource

  FilterInvocationSecurityMetadataSource元数据,接口获取的是全部URL的权限信息和当http请求过来时候url对应的权限。其中getAttributes的参数o包含了当前访问的URL,根据URL我们需要返回对应的权限信息;getAllConfigAttributes会在项目启动时执行一次,获取所有URL对应的权限信息。

package com.auth.frame.security.processor;

import com.auth.frame.security.bean.Role;
import com.auth.frame.security.repository.RoleRepository;
import com.auth.frame.security.service.URLService;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import static com.auth.frame.security.config.ConstValue.ROLE.DEFAULT_ROLE;

/**
 * @ClassName CSTSecurityMetadataSource
 * @Author WFS
 * @Date 2020/1/21 17:06
 */
public class CSTSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private URLService urlService;

    private RoleRepository roleRepository;

    public CSTSecurityMetadataSource(URLService urlService, RoleRepository roleRepository) {
        this.urlService = urlService;
        this.roleRepository = roleRepository;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        String url = ((FilterInvocation) o).getRequestUrl();
        Collection<ConfigAttribute> configAttributes = new HashSet<>();
        Set<Role> roles = urlService.getCacheURL(url);
        roles.forEach(ele -> configAttributes.add(new SecurityConfig(ele.getAuth())));
        if (configAttributes.isEmpty()) {
            configAttributes.add(new SecurityConfig(DEFAULT_ROLE));
        }
        return configAttributes;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        Collection<ConfigAttribute> configAttributes = new HashSet<>();
        roleRepository.findAllByDeletedIsFalse().forEach(e -> configAttributes.add(new SecurityConfig(e.getAuth())));
        return configAttributes;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}
  1. AccessDecisionManager

  投票管理器,接口规定了用户权限和URL权限之间的逻辑,即满足某种情况拒绝或者允许。

package com.auth.frame.security.processor;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;

import java.util.Collection;

import static com.auth.frame.security.config.ConstValue.ROLE.DEFAULT_ROLE;

/**
 * @ClassName CSTAccessDecisionManager
 * @Author WFS
 * @Date 2020/1/21 17:06
 */
public class CSTAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        if (collection.size() == 1 && collection.stream().anyMatch(ele -> DEFAULT_ROLE.equals(ele.getAttribute()))) {
            return;
        }
        if (!collection.stream().allMatch(ele -> authentication.getAuthorities().stream().anyMatch(e -> ele.getAttribute().equals(e.getAuthority())))) {
            throw new AccessDeniedException("AccessDenied!");
        }
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return Boolean.TRUE;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return Boolean.TRUE;
    }
}

  Authentication中包含有用户的权限信息,Collection<ConfigAttribute>包含有当前URL需要的权限信息,根据这两个参数我们就可以自由的指定我们投票规则。比如:某个URL含有多个权限,某个用户只要拥有其中一个就可以访问,或者要求必须全部拥有对应的权限才可以访问,此处可以自由发挥。需要注意的是,AccessDecisionManager不起作用的情况,原因是如果Collection<ConfigAttribute>null或者empty,则不会执行投票器,所以我们需要给没有配置权限的URL一个默认权限。

tokenFilter

  tokenFilter主要用来检查是否登陆的逻辑。先从请求里面获取token,解密,然后从reids里面获取相关权限信息;如果没有token则检查是否是用户名和密码,如果存在那么验证用户名和密码是否匹配,然后生成授权信息,存入redis,返回token给前端。

package com.auth.frame.security.processor;

import com.auth.frame.common.ResponseEntity;
import com.auth.frame.common.Utils.JSONUtil;
import com.auth.frame.common.Utils.StringUtil;
import com.auth.frame.security.Utils.TokenUtil;
import com.auth.frame.security.bean.User;
import com.auth.frame.security.repository.UserRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName TokenFilter
 * @Author WFS
 * @Date 2020/1/21 10:47
 */
@Component
public class TokenFilter implements Filter {


    private final UserRepository userRepository;

    @Resource(name = "sysTokenRedisTemplate")
    public RedisTemplate<String, String> redisTemplate;

    private static final String usernameParameter = "username";
    private static final String passwordParameter = "password";

    public TokenFilter(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String rds = TokenUtil.getBody(request);
        if (rds == null) {
            String username = request.getParameter(usernameParameter);
            String password = request.getParameter(passwordParameter);
            if (!StringUtil.isNullOrEmpty(username, password)) {
                User user = userRepository.findByDeletedIsFalseAndUserName(username);
                if (user != null) {
                    if (user.getAccountNonExpired() && user.getAccountNonLocked() && user.getCredentialsNonExpired() && user.getEnabled()) {
                        if (!StringUtil.isNullOrEmpty(user.getSalt(), user.getPassWord())) {
                            if (user.getPassWord().equals(DigestUtils.md5DigestAsHex((password + user.getSalt()).getBytes(StandardCharsets.UTF_8)))) {
                                Set<String> rdsSet = redisTemplate.keys("*");
                                if (rdsSet != null) {
                                    for (String s : rdsSet) {
                                        String rc = redisTemplate.opsForValue().get(s);
                                        if (rc != null) {
                                            CSTUserDetails rc_u = JSONUtil.json2Obj(rc, CSTUserDetails.class);
                                            if (rc_u != null) {
                                                if (user.getId().equals(rc_u.getUserId())) {
                                                    redisTemplate.delete(s);
                                                }
                                            }
                                        }
                                    }
                                }
                                CSTUserDetails cstUserDetails = new CSTUserDetails(user);
                                String r = UUID.randomUUID().toString().replaceAll("-", "");
                                redisTemplate.opsForValue().set(r, JSONUtil.obj2Json(cstUserDetails));
                                redisTemplate.expire(r, 2, TimeUnit.HOURS);
                                String token = TokenUtil.create(r);
                                ResponseEntity.ofToken(response, token);
                                return;
                            }
                        }
                    }
                }
            }
        } else {
            String cuStr = redisTemplate.opsForValue().get(rds);
            if (!StringUtil.isNullOrEmpty(cuStr)) {
                CSTUserDetails cu = JSONUtil.json2Obj(cuStr, CSTUserDetails.class);
                if (cu != null) {
                    if (cu.getAccountNonExpired() && cu.getAccountNonLocked() && cu.getCredentialsNonExpired() && cu.getEnabled()) {
                        SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(cu.getUserId(), cu.getPassword(), cu.getAuthorities()));
                    }
                }
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}
  1. TokenUtil 主要从request里面解析token里面的rds
  2. 中间有一段redis的操作,此处可以自行拓展,即每一个新的附带密码的请求都去删除redis里面相同userId的授权信息,这样做的好处是可以保证每个账号同一个时间只有一个人登陆。
  3. ResponseEntity.ofToken() 工具类,用于向response返回信息

配置spring security

  设置withObjectPostProcessor来设置AccessDecisionManagerSecurityMetadataSourceaddFilterBefore设置TokenFilter的位置,web.ignoring设置开放API的路径,开放API不受spring security过滤链的控制。

package com.auth.frame.security.config;

import com.auth.frame.security.processor.CSTAccessDecisionManager;
import com.auth.frame.security.processor.CSTSecurityMetadataSource;
import com.auth.frame.security.processor.CSTUserDetailsService;
import com.auth.frame.security.processor.TokenFilter;
import com.auth.frame.security.repository.RoleRepository;
import com.auth.frame.security.service.URLService;
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.authentication.builders.AuthenticationManagerBuilder;
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.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsUtils;

/**
 * @ClassName SecurityConfig
 * @Author WFS
 * @Date 2020/1/21 10:25
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    private final CSTUserDetailsService cstUserDetailsService;
    private final ApplicationSecurityConfig applicationSecurityConfig;
    private final TokenFilter tokenFilter;

    private final URLService urlService;
    private final RoleRepository roleRepository;


    public SecurityConfig(CSTUserDetailsService cstUserDetailsService, ApplicationSecurityConfig applicationSecurityConfig, TokenFilter tokenFilter, URLService urlService, RoleRepository roleRepository) {
        this.cstUserDetailsService = cstUserDetailsService;
        this.applicationSecurityConfig = applicationSecurityConfig;
        this.tokenFilter = tokenFilter;
        this.urlService = urlService;
        this.roleRepository = roleRepository;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(cstUserDetailsService);
    }

    @Bean
    public CSTAccessDecisionManager getCSTAccessDecisionManager() {
        return new CSTAccessDecisionManager();
    }

    @Bean
    public CSTSecurityMetadataSource getCSTSecurityMetadataSource() {
        return new CSTSecurityMetadataSource(urlService, roleRepository);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().cors()
                .and().authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setAccessDecisionManager(getCSTAccessDecisionManager());
                        o.setSecurityMetadataSource(getCSTSecurityMetadataSource());
                        return o;
                    }
                })
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                .anyRequest().authenticated()
                .and().anonymous().disable().formLogin().permitAll()
                .and().addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().regexMatchers(applicationSecurityConfig.getOpenApi());
    }
}

结语

  通过以上逻辑,token登陆和自定义权限功能已经完成,其实所有的功能实现集中于UserDetails的构建和AccessDecisionManager投票逻辑,熟知于此就可以随意拓展Filter来实现自己的功能。