Skip to content
🔴🟠🟡🟢🔵🟣🟤⚫⚪

spring-cloud-alibaba-demo

十四、springsecurity安全认证授权

https://juejin.cn/post/7118206768634675207

https://juejin.cn/post/6952128540208955405

引入依赖

java
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

第一次引入依赖后,所有的接口访问都需要认证

会默认跳转到登录接口http://ip:port/login

默认退出登录接口 http://ip:port/logout

登录校验流程

springSecurity完整流程

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException

FilterSecuritylnterceptor:负责权限校验的过滤器

认证流程

自定义UserDetailService从数据库中通过登录账号查询密码,返回后与Authentication对象里面的密码对比

登录 ①自定义登录接口 调用ProviderManager的方法进行认证如果认证通过生成jwt 把用户信息存入redis中 ②自定义UserDetailsService 在这个实现列中去查询数据

校验 ①定义Jwt认证过滤器 ②获取token ③解析token获取其中的userid ④从redis中获取用户信息 ⑤存入SecurityContextHolder(定义过滤器存入这里面)

java
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>

        <!--  jwt 认证 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.16.0</version>
        </dependency>

redis使用fastjson序列化

java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * @author zb
 * @date 2022/3/10 0:25
 * @Description Redis 使用 FastJson 序列化
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

    private Class<T> tClass;

    static {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> tClass){
        super();
        this.tClass = tClass;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        try {
            return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
        } catch (Exception e) {
            throw new SerializationException("Could not serialize: " + e.getMessage(), e);
        }
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0){
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        try {
            return JSON.parseObject(str, tClass);
        } catch (Exception e) {
            throw new SerializationException("Could not deserialize: " + e.getMessage(), e);
        }
    }

    protected JavaType getJavaType(Class<?> tClass){
        return TypeFactory.defaultInstance().constructType(tClass);
    }
}
java
import cn.mesmile.system.serializer.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author zb
 * @date 2022/3/10 0:35
 * @Description fastjson序列化
 *              KryoRedisSerializer的压缩率和速度最优,fastJson次之,默认的则最差
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<String,Object> template  = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);

        // 使用 StringRedisSerializer 来序列化和反序列化 Redis 的key
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(fastJsonRedisSerializer);

        // hash的key 也使用 StringRedisSerializer 来序列化和反序列化 Redis 的key
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(fastJsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }

自定义类实现UserDetailsService

java
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 通过账号查询用户
        LambdaQueryWrapper<User> eq = Wrappers.<User>lambdaQuery()
                .eq(User::getUsername, username);
        User user = userService.getOne(eq);
        if (user == null){
            throw new ServiceException("用户名或密码错误");
        }
        if (!user.getEnabled()){
            throw new ServiceException("用户已禁用,请联系管理员");
        }
        // todo 通过用户查询权限


        return new UserLogin(user);
    }

}

自定类实现 UserDetails

java
@NoArgsConstructor
@AllArgsConstructor
@Data
public class UserLogin implements UserDetails {

    private User user;

    /**
     * 返回用户的授权信息
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    /**
     *  返回用户密码
     * @return
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    /**
     * 返回用户名
     * @return
     */
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    /**
     * 判断账号是否 没过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 判断账号是否 没锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 证书是否 没过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

springsecurity有一个默认的密码编码,若使用明文密码则需要在数据库密码前加上

密码加密存储

实际项目中我们不会把密码明文存储在数据库中。 默认使用的Password Encoder要求数据库中的密码格式为:{id} password。 它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。

我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中, SpringSecurity就会使用该PasswordEncoder来进行密码校验。

我们可以定义一个SpringSecurity的配置类, SpringSecurity要求这个配置类要继承WebSecurityConf gurerAdapter。

java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

Spring Security认证

接下我们需要自定义登陆接口, 然后让SpringSecurity对这个接口放行, 让用户访问这个接口的时候不用登录也能访问。

在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证, 所以需要在SecurityConfig中配置把 AuthenticationManager注入容器

认证成功的话要生成一个jwt, 放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户, 我们需要把用户信息存入redis, 可以把用户id作为key。

登录 ①自定义登录接口 调用ProviderManager的方法进行认证如果认证通过生成jwt 把用户信息存入redis中 ②自定义UserDetailsService 在这个实现列中去查询数据

自定义登录接口

java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    // 自定义认证管理,自定义 认证管理
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    
    /**
     * 配置 放行接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http);
        http    // 前后端分离 关闭 csrf
                .csrf().disable()
                // 前后端分离 session不管用   不通过 Session 获取 SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除了上面的请求,其他请求都需要鉴权认证
                .anyRequest().authenticated();

        // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前
        // http
                //.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


    }

}

登录service方法

java
@Override
    public String login(User user) {
        String principal = user.getUsername();
        String credentials = user.getPassword();
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(principal, credentials);
        // 用户认证 AuthenticationManager authenticate 进行用户认证
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (authenticate == null){
            throw new ServiceException("登录失败");
        }
        // UserLogin 实现了 UserDetails 接口
        // 认证前 principal  为username ,认证后 principal 为 UserDetails 的实现类
        UserLogin userLogin = (UserLogin) authenticate.getPrincipal();
        // user = userLogin.getUser();


        // 使用username生成一个jwt 存入返回结果
        String token = JwtUtil.sign(user.getUsername());

        // 把完整用户信息存入 redis userid作为key
        cloudRedisUtil.set(user.getUsername(),userLogin);
        return token;
    }

定义认证过滤器

JwtAuthenticationTokenFilter

java
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final CloudRedisUtil cloudRedisUtil;

    public JwtAuthenticationTokenFilter(CloudRedisUtil cloudRedisUtil) {
        this.cloudRedisUtil = cloudRedisUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 解析token
        String token = request.getHeader("Authorization");
        // 如果为空放行
        if (StringUtils.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        boolean tokenExpired = JwtUtil.isTokenExpired(token);
        if (tokenExpired){
            filterChain.doFilter(request, response);
            return;
        }

        String username = JwtUtil.getClaim(token, "username");
        // redis中获取用户
        UserLogin userLogin = (UserLogin) cloudRedisUtil.get(username);
        if (userLogin == null){
            filterChain.doFilter(request, response);
            return;
            // throw new RuntimeException("用户未登录");
        }
        

        SecurityContext securityContext = SecurityContextHolder.getContext();
        // todo 查询权限放入
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userLogin, null, null);
        // 将认证信息存入 SecurityContextHolder
        securityContext.setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(request, response);
    }

}

配置认证过滤器

// 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前

java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
    }

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

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置 放行接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // super.configure(http);
        http    // 前后端分离 关闭 csrf
                .csrf().disable()
                // 前后端分离 session不管用   不通过 Session 获取 SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除了上面的请求,其他请求都需要鉴权认证
                .anyRequest().authenticated();

        // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);


    }
}

自定义退出登录

我们只需要定义一个登陆接口, 然后获取SecurityContextHolder中的认证信息, 删除redis中对应的数据即可。

默认的 /logout 路径不可使用,spring security的LogoutFilter 已经占用,可以重写退出登录拦截器和退出登录成功拦截器

// 退出登录处理 ,配置退出成功拦截器 http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);

若想使用自定义退出逻辑则可以再配置里面关闭LogoutFilter http.logout().disable();

自定义退出登录方法

java
@Override
    public boolean logout() {
        // 调用注销接口的时候需要携带token
        // 从 SecurityContextHolder 请求中获取认证信息,然后再获取username
        SecurityContext context = SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        if (authentication == null){
            return false;
        }
        // 认证之后 principal 里面是 UserDetails 的子类
        // 未认证的时候 principal 里面是 username (登录账号)
        Object principal = authentication.getPrincipal();
        // UserLogin 实现了 UserDetails 接口
        UserLogin userLogin = (UserLogin) principal;
        User user = userLogin.getUser();
        String username = user.getUsername();
        // 删除redis中的token
        return cloudRedisUtil.delete(username);
    }

Spring Security授权权限

在Spring Security中, 会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication, 然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication

java
SecurityContext context = SecurityContextHolder.getContext();
 Authentication authentication = context.getAuthentication();

基于注解的方式限制访问资源权限

开启全局基于注解方式的权限认证:

权限判断注解@PreAuthorize

java
// 判断用户是否有 test 这个权限,必须具有 test 这个权限才可以访问
    @PreAuthorize("hasAuthority('test')")
    @GetMapping("/list")
    public R listUser(){
        List<User> list = userService.listUser();
        return R.data(list);
    }

其他方式进行权限认证

前面都是使用@PreAuthorize注解, 然后在在其中使用的是hasAuthority方法进行校验。Spring Security还为我们提供了其它方法 例如:hasAnyAuthority, hasRole, hasAnyRole, 等。 这里我们先不急着去介绍这些方法, 我们先去理解hasAuthority的原理, 然后再去学习其他方法你就更容易理解, 而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。

hasAuthority方法实际是执行到了SecurityExpressionRoothasAuthority, 大家只要断点调试既可知道它内部的校验原理。 它内部其实是调用authenticationgetAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。

hasAnyAuthority方法可以传入多个权限, 只有用户有其中任意一个权限都可以访问对应资源。

java
@PreAuthorize("hasAnyAuthority('title:list','title:list2')")
    @GetMapping("/list")
    public R listUser(){
        List<User> list = userService.listUser();
        return R.data(list);
    }

hasRole要求有对应的角色才可以访问, 但是它内部会把我们传入的参数拼接上**ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_**这个前缀才可以。数据库中代码角色的权限要加上 ROLE_

代码里面不用添加,以下情况就能访问:

java
@PreAuthorize("hasAnyRole('title:list2')")
    @GetMapping("/list")
    public R listUser(){
        List<User> list = userService.listUser();
        return R.data(list);
    }

hasAnyRole有任意的角色就可以访问。它内部也会把我们传入的参数拼接上ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。

自定义权限校验方法

自定义权限校验方法 CloudSecurityExpression

java
/**
 * @author zb
 * @Description 自定义权限校验方法
 */
@Component("cloudSecurityExpression")
public class CloudSecurityExpression {

    public final boolean hasAuthority(String authority){
        return hasAnyAuthority(authority);
    }

    private boolean hasAnyAuthority(String... authoritys) {
        // 获取拥有的认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserLogin userLogin = (UserLogin) authentication.getPrincipal();
        Set<String> permissionSet = userLogin.getPermissionSet();
        for (String authority : authoritys) {
            if (permissionSet.contains(authority)) {
                return true;
            }
        }
        return false;
    }

    public final boolean hasRole(String role) {
        return hasAnyRole(role);
    }

    private boolean hasAnyRole(String... roles) {
        // 获取拥有的认证信息
        return hasAnyAuthority(roles);
    }

}

使用自定义权限校验

java
@PreAuthorize("@cloudSecurityExpression.hasAuthority('title:list2')")
@GetMapping("/list")
public R listUser(){
    List<User> list = userService.listUser();
    return R.data(list);
}

基于配置的权限控制

java
/**
 * @author zb
 * @Description 配置 springSecurity密码加密的方式
 *      @EnableGlobalMethodSecurity(prePostEnabled = true)  开启基于注解的权限授权控制
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    private final AccessDeniedHandlerImpl accessDeniedHandler;

    private final AuthenticationEntryPointImpl authenticationEntryPoint;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, AccessDeniedHandlerImpl accessDeniedHandler, AuthenticationEntryPointImpl authenticationEntryPoint) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.accessDeniedHandler = accessDeniedHandler;
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

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

    /**
     * 自定义认证管理 登录、退出
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置 放行接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】
        // super.configure(http);

        http    // 前后端分离 关闭 csrf
                .csrf().disable()
                // 前后端分离 session不管用   不通过 Session 获取 SecurityContext 这里关闭 session 功能
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问,当携带token的时候不能访问
                // .permitAll() 表示 登录和未登录 都可以访问
                .antMatchers("/user/login").anonymous()
                // 除了上面的请求, .authenticated() 任意用户都可以认证后访问
                .anyRequest().authenticated();

        // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 将自定义的 认证 和 授权 异常处理加入到配置
        http.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);

        // 开启springSecurity允许跨域
        http.cors();
    }
}

查询到用户的权限信息放入 UserLogin UserDetailsServiceImpl

java
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 通过账号查询用户
        LambdaQueryWrapper<User> eq = Wrappers.<User>lambdaQuery()
                .eq(User::getUsername, username);
        User user = userService.getOne(eq);
        if (user == null){
            throw new ServiceException("用户名或密码错误");
        }
        if (!user.getEnabled()){
            throw new ServiceException("用户已禁用,请联系管理员");
        }
        // todo 通过用户从数据库中查询权限
        List<String> permissionList = new ArrayList<>(Arrays.asList("test", "admin"));


        return new UserLogin(user, permissionList);
    }

}

UserLogin接收转换权限

java
@NoArgsConstructor
@Data
public class UserLogin implements UserDetails {

    /**
     * 用户
     */
    private User user;

    /**
     * 权限集合
     */
    private List<String> permissionList;

    public UserLogin(User user, List<String> permissionList){
        this.user = user;
        this.permissionList = permissionList;
    }

    /**
     * 此变量无需序列化
     */
    @JSONField(serialize = false)
    private Set<SimpleGrantedAuthority> grantedAuthorities;

    /**
     * 返回用户的授权信息
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (grantedAuthorities == null){
            grantedAuthorities = permissionList.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toSet());
        }
        return grantedAuthorities;
    }

    /**
     *  返回用户密码
     * @return
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    /**
     * 返回用户名
     * @return
     */
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    /**
     * 判断账号是否 没过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 判断账号是否 没锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 证书是否 没过期
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

JwtAuthenticationTokenFilter 过滤器放入权限

java
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final CloudRedisUtil cloudRedisUtil;

    public JwtAuthenticationTokenFilter(CloudRedisUtil cloudRedisUtil) {
        this.cloudRedisUtil = cloudRedisUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 解析token
        String token = request.getHeader("Authorization");
        // 如果为空放行
        if (StringUtils.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        boolean tokenExpired = JwtUtil.isTokenExpired(token);
        if (tokenExpired){
            filterChain.doFilter(request, response);
            return;
        }
        String username = JwtUtil.getClaim(token, "username");
        // redis中获取用户
        UserLogin userLogin = (UserLogin) cloudRedisUtil.get(username);
        if (userLogin == null){
            filterChain.doFilter(request, response);
            return;
            // throw new RuntimeException("用户未登录");
        }
        SecurityContext securityContext = SecurityContextHolder.getContext();

        // todo 查询权限放入
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userLogin, null, userLogin.getAuthorities());
        // 将认证信息存入 SecurityContextHolder
        securityContext.setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(request, response);
    }

}

RBAC权限模型

RBAC权限模型(Role-Based AccessControl) 即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

用户 多对多 角色 多对多 权限

自定义失败处理

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json, 这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道Spring Security的异常处理机制。

在Spring Security中, 如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成AccessDeniedException 然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理, 我们只需要自定义AuthenticationEntryPointAccessDeniedHandler然后配置给SpringSecurity即可。

自定义 认证异常 拦截处理

java
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        R<Object> r = R.fail(ResultCode.CLIENT_UN_AUTHORIZED, "请求未认证");
        String result = JSONObject.toJSONString(r);

        WebUtil.renderString(response, result);
    }

}

自定义 授权异常 拦截处理

java
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
        R<Object> r = R.fail(ResultCode.REQ_REJECT, "请求未授权");
        String result = JSONObject.toJSONString(r);

        WebUtil.renderString(response, result);
    }
}
java
public static String renderString(HttpServletResponse response,String text) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().print(text);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

加入到security配置中

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

    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    private final AccessDeniedHandlerImpl accessDeniedHandler;

    private final AuthenticationEntryPointImpl authenticationEntryPoint;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, AccessDeniedHandlerImpl accessDeniedHandler, AuthenticationEntryPointImpl authenticationEntryPoint) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.accessDeniedHandler = accessDeniedHandler;
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

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

    /**
     * 自定义认证管理 登录、退出
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置 放行接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】
        // super.configure(http);

        http    // 前后端分离 关闭 csrf
                .csrf().disable()
                // 前后端分离 session不管用   不通过 Session 获取 SecurityContext 这里关闭 session 功能
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问,当携带token的时候不能访问
                // .permitAll() 表示 登录和未登录 都可以访问
                .antMatchers("/user/login").anonymous()
                // 除了上面的请求, .authenticated() 任意用户都可以认证后访问
                .anyRequest().authenticated();

        // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 将自定义的 认证 和 授权 异常处理加入到配置
        http.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);

    }
}

配置支持跨域请求

java
/**
 * @author zb
 * @Description 配置支持跨域请求
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOrigins("*")
                // 设置是否允许cookie
                .allowCredentials(true)
                // 设置允许请求方式
                .allowedMethods("GET","POST","DELETE","PUT")
                // 设置允许的 header 属性
                .allowedHeaders("*")
                // 设置跨域允许时间
                .maxAge(3600);
    }
}

开启springSecurity允许跨域

// 开启springSecurity允许跨域 http.cors();

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

    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    private final AccessDeniedHandlerImpl accessDeniedHandler;

    private final AuthenticationEntryPointImpl authenticationEntryPoint;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, AccessDeniedHandlerImpl accessDeniedHandler, AuthenticationEntryPointImpl authenticationEntryPoint) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.accessDeniedHandler = accessDeniedHandler;
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

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

    /**
     * 自定义认证管理 登录、退出
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 配置 放行接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】
        // super.configure(http);

        http    // 前后端分离 关闭 csrf
                .csrf().disable()
                // 前后端分离 session不管用   不通过 Session 获取 SecurityContext 这里关闭 session 功能
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问,当携带token的时候不能访问
                // .permitAll() 表示 登录和未登录 都可以访问
                .antMatchers("/user/login").anonymous()
                // 除了上面的请求, .authenticated() 任意用户都可以认证后访问
                .anyRequest().authenticated();

        // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 将自定义的 认证 和 授权 异常处理加入到配置
        http.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);

        // 开启springSecurity允许跨域
        http.cors();
    }
}

CSRF攻击

前后端【不分离】的项目,通常包验证信息存放在cookie中,导致如下图的攻击:

前后端分离的信息,利用每次请求头中 带上 token 信息,天然的防范的csrf 攻击

SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token, 前端发起请求的时候需要携带这个csrf_token, 后端会有过滤器进行校验, 如果没有携带或者是伪造的就不允许访问

可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token, 而token并不是存储中cookie中, 并且需要前端代码去把token设置到请求头中才可以, 所以CSRF攻击也就不用担心了

因此可以关闭csrf,不然框架会检查 csrf_token

// 前后端分离 关闭 csrf

http.csrf().disable()

java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
     * 配置 放行接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】
        // super.configure(http);

        http    // 前后端分离 关闭 csrf
                .csrf().disable()
                // 前后端分离 session不管用   不通过 Session 获取 SecurityContext 这里关闭 session 功能
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问,当携带token的时候不能访问
                // .permitAll() 表示 登录和未登录 都可以访问
                .antMatchers("/user/login").anonymous()
                // 除了上面的请求, .authenticated() 任意用户都可以认证后访问
                .anyRequest().authenticated();

        // 将认证过滤器放在 UsernamePasswordAuthenticationFilter 之前
        http
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 将自定义的 认证 和 授权 异常处理加入到配置
        http.exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint);

        // 开启springSecurity允许跨域
        http.cors();
    }
}

登录成功处理器(一般没使用)

实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候, 如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。我们也可以自己去自定义成功处理器进行成功后的相应处理。

自定义类实现登录成功拦截器 AuthenticationSuccessHandler

java
/**
 * @author zb
 * @date 2022/3/11 15:50
 * @Description 自定原生的登录成功处理器
 *               注意:配置了
 *
 *               @Bean
 *               @Override
 *               public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
 *
 *               后就不会走 UsernamePasswordAuthenticationFilter 过滤器,就不会再调用登录成功拦截器了 AuthenticationSuccessHandler
 */
@Component
public class CloudSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Object principal = authentication.getPrincipal();
        System.out.println(">>>>>>>>>>>>>>>> 登录成功:"+ principal.toString());
    }

}

配置自定义登录成功拦截器

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

    @Autowired
    private CloudSuccessHandler cloudSuccessHandler;
    /**
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】
        // super.configure(http);
        
        // 配置登录成功,登录成功拦截器
        http.formLogin().successHandler(cloudSuccessHandler);
        http.authorizeRequests().anyRequest().authenticated();
    }
}

登录失败处理器(一般不用)

实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候, 如果登录失败了是会调用AuthenticationFailureHandler 的方法进行认证失败后的处理的。AuthenticationFailureHandler 就是登录失败处理器。我们也可以自己去自定义失败处理器进行失败后的相应处理。

注意注释内容

java
/**
 * @author zb
 *     自定原生的登录失败处理器
 *               注意:配置了
 *
 *               @Bean
 *               @Override
 *               public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
 *
 *               后就不会走 UsernamePasswordAuthenticationFilter 过滤器,就不会再调用登录成功拦截器了 AuthenticationSuccessHandler
 */
 */
public class CloudAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println("-----------进入自定义登录失败拦截器------------");
    }
}

配置自定义登录成功拦截器

java
```Java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CloudAuthenticationFailureHandler failureHandler;
    /**
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】
        // super.configure(http);
        
        // 配置登录失败,登录失败拦截器
        http.formLogin().failureHandler(failureHandler);
        http.authorizeRequests().anyRequest().authenticated();
    }
}
```

退出登录成功处理器

自定义退出登录成功拦截器

java
/**
 * @author zb
 * @Description 自定原生的退出登录成功处理器
 *                 注意:配置了
 *
 *                 @Bean
 *                 @Override
 *                 public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
 *
 *                 后就不会走 LogoutFilter 过滤器,就不会再调用退出登录成功拦截器了 LogoutSuccessHandler
 */
@Component
public class CloudLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("------------进入到 退出登录成功了-------------");
    }

}

配置自定义退出登录拦截器

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

    @Autowired
    private CloudLogoutSuccessHandler cloudLogoutSuccessHandler;

    /**
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 不建议调用 super 类中的配置,因为里面有默认配置,不适用于当前【前后端分离业务】
        // super.configure(http);

        http.logout().logoutSuccessHandler(cloudLogoutSuccessHandler);

    }
}

自动登录(前后端不分离的情况)

SpringCloud权限认证

用户分配角色 角色分配权限

https://www.bilibili.com/video/BV15a411A7kP?p=28