SpringSecurity6的配置使用

认证

在这里插入图片描述

1. 首先引入SpringSecurity的依赖

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

引入依赖后所有的请求都会被Security拦截,需要登录才能访问(登录用户为user,密码会启动时生成随机字符串)

由于SpringSecurity默认走的是内存验证用户名和密码,获取用户名密码和权限的接口是UserDetailsService

2. 重写UserDetailsService接口的loadUserByUsername方法

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 通过用户名查询用户信息
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(User::getUsername, username);
    User user = userMapper.selectOne(queryWrapper);

    if (Objects.isNull(user)) {
        throw new LoginException("用户名错误");
    }

    // TODO 根据用户查询权限信息 添加到LoginUser中,授权的时候做

    // 封装为UserDetails的实现类
    return new LoginUser(user, authorityNameList);
}

3. 因为返回的是UserDetails类型是一个接口,需要实现这个接口

package com.big.event.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.jsonwebtoken.lang.Collections;
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;
import java.util.List;
import java.util.Objects;

/**
 * @author notch
 * @create 2024-05-24-10:58
 */
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // TODO 权限信息会在授权的时候传入
        return null;
    }

    @Override
    public String getPassword() { // 密码
        return user.getPassword();
    }

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

    @Override
    public boolean isAccountNonExpired() { // 账户是否未过期
        return true;
    }

    @Override
    public boolean isAccountNonLocked() { // 账户是否未被锁定
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() { // 凭据是否未过期
        return true;
    }

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

重写后由于SpringSecurity支持多种密码的验证方式所以,如果数据库的密码为明文需要添加{noop}密码

4. 重写SpringSecurity的密码加密规则,一般使用 BCryptPasswordEncoder

@Configuration
public class SecurityConfig {

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

}

SpringSecurity在更新到6以后,原来配置类需要继承的WebSecurityConfigurerAdapter 类过期了,目前的配置方式是直接将需要重写的方法定义为Bean

5. 自定义登录接口,替换默认登录接口

(1) 定义登录接口

package com.big.event.controller;

import com.big.event.common.resp.LoginResp;
import com.big.event.common.resp.Result;
import com.big.event.entity.User;
import com.big.event.service.LoginService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author notch
 * @create 2024-05-21-17:46
 */
@RestController
@Tag(name = "登录相关接口")
public class LoginController {

    @Autowired
    private LoginService loginService;

    @PostMapping("/user/login")
    @Operation(description = "登录接口")
    public Result<LoginResp> login(@RequestBody @Valid User user) {
        return loginService.login(user);
    }
}

(2) 在创建实现类之前,由于需要调用原来的认证逻辑,通过AuthenticationManager的authenticate()方法,但它不是一个Bean,需要先重写它将他注册成为Bean

@Configuration
public class SecurityConfig {

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

}

(3) 实现登录接口

package com.big.event.service.impl;

import com.big.event.common.exceptions.LoginException;
import com.big.event.common.resp.LoginResp;
import com.big.event.common.resp.Result;
import com.big.event.common.utils.JWTTokenUtil;
import com.big.event.common.utils.RedisUtil;
import com.big.event.entity.LoginUser;
import com.big.event.entity.User;
import com.big.event.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;

import java.util.Objects;

/**
 * @author notch
 * @create 2024-05-24-16:37
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisUtil redisUtil;

    @Override
    public Result<LoginResp> login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = null;
        try {
            authenticate = authenticationManager.authenticate(authenticationToken);
        } catch (AuthenticationException e) {
            throw new LoginException(e.getMessage());
        }
        // 使用用户id生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        // 生成jwt
        User user1 = loginUser.getUser();
        String accessToken = JWTTokenUtil.createAccessToken(user1);
        // 存入redis
        redisUtil.setLoginUser(user1.getId(), loginUser);
        // 返回token
        LoginResp loginResp = new LoginResp();
        loginResp.setToken(accessToken);
        return Result.success(loginResp);
    }
}

这里将用户信息存入redis,是为了方便后面过滤器填入,SecurityContextHolder的上下文时方便获取数据

(4) 配置登录接口和Swgger请求路径不校验

@Configuration
public class SecurityConfig {

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((authorizeHttpRequests) -> {
                    authorizeHttpRequests.requestMatchers("/swagger-ui/**", "/v3/api-docs/swagger-config", "/v3/api-docs", "/user/login").permitAll() // 放行我自己的登录接口和swagger接口,不需要认证和授权
                            .anyRequest().authenticated(); // 其他请求都需要授权后才能使用(如果不写会被过滤器拦截除上面配置请求的所有请求)
                })
                .csrf(AbstractHttpConfigurer::disable) // 关闭跨域
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                ) // 不同过session创建管理SecurityContextHolder

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

}

6. 定义所有请求的过滤器

(1) 向SecurityContextHolder上下文中填写用户信息

@Component
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("Authorization");
        // 判断token是否存在
        if (!StringUtils.hasText(token)) {
            // 直接放行
            filterChain.doFilter(request, response);
            return;
        }
        // 去掉Bearer
        token = token.replaceAll("Bearer ", "");
        // 解析token,获取用户信息
        User user1 = JWTTokenUtil.parseToken(token);
        if (Objects.nonNull(user1)) {
            // 获取用户信息
            LoginUser loginUser = redisUtil.getLoginUser(user1.getId());
            // 写入
            if (Objects.nonNull(loginUser)) {
                SecurityFrameworkUtils.setLoginUser(loginUser); // 自定义写入工具类
            }
        }
        filterChain.doFilter(request, response);
    }
}

(2) 将过滤器加入SpringSecurity的过滤器链中

@Configuration
public class SecurityConfig {

    @Autowired
    private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((authorizeHttpRequests) -> {
                    authorizeHttpRequests.requestMatchers("/swagger-ui/**", "/v3/api-docs/swagger-config", "/v3/api-docs", "/user/login").permitAll() // 放行我自己的登录接口和swagger接口
                            .anyRequest().authenticated(); // 其他请求都需要授权后才能使用(如果不写会被过滤器拦截除上面配置请求的所有请求)
                })
                .csrf(AbstractHttpConfigurer::disable) // 关闭跨域
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                ) // 不同过session创建管理SecurityContextHolder
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加过滤器

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

}

注意:如果没有配置.anyRequest().authenticated(),会被AuthorizationFilter里面的this.authorizationManager.check(this::getAuthentication, request);拦截然后这些除了配置的请求会全部认证授权不通过

授权

1. 去UserDetialsServiceImpl中的loadUserByUsername()方法里面添加用户权限

package com.big.event.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.big.event.common.exceptions.LoginException;
import com.big.event.entity.LoginUser;
import com.big.event.entity.User;
import com.big.event.mapper.UserMapper;
import com.big.event.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * @author notch
 * @create 2024-05-24-10:10
 */
@Service
public class UserDetialsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 通过用户名查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUsername, username);
        User user = userMapper.selectOne(queryWrapper);

        if (Objects.isNull(user)) {
            throw new LoginException("用户名错误");
        }

        // 根据用户查询权限信息 添加到LoginUser中
        List<String> authorityNameList = userMapper.selectUserAuthority(user.getId());

        // 封装为UserDetails的实现类
        return new LoginUser(user, authorityNameList);
    }
}

2. LoginUser写入权限信息

package com.big.event.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.jsonwebtoken.lang.Collections;
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;
import java.util.List;
import java.util.Objects;

/**
 * @author notch
 * @create 2024-05-24-10:58
 */
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private List<String> authorityNameList;

    @JsonIgnore
    private Collection<? extends GrantedAuthority> grantedAuthorities;

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

    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        if (!Collections.isEmpty(grantedAuthorities)) {
            return grantedAuthorities;
        }
        grantedAuthorities = authorityNameList.stream().map(SimpleGrantedAuthority::new).toList();
        return grantedAuthorities;
    }

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

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

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

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

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

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

注意:如果存入redis的是LoginUser要注意不能把GrantedAuthority他的实现类写入redis中,因为他的实现类是没有无参构造方法的,而GenericJackson2JsonRedisSerializer序列化器是通过无参构造方法创建对象后,使用反射填入属性值的,所以反序列化的时候会报错

3. 在自定义过滤器中,向SecurityContextHolder上下文填入权限信息

public static void setLoginUser(LoginUser loginUser) {
    // 用户登录信息和权限信息填入
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}

4. 开启注解权限校验并且在需要校验的接口上添加权限信息

(1) 开启注解权限校验

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((authorizeHttpRequests) -> {
                    authorizeHttpRequests.requestMatchers("/swagger-ui/**", "/v3/api-docs/swagger-config", "/v3/api-docs", "/user/login").permitAll() // 放行我自己的登录接口和swagger接口
                            .anyRequest().authenticated(); // 其他请求都需要授权后才能使用
                })
                .csrf(AbstractHttpConfigurer::disable) // 关闭跨域
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                ) // 不同过session创建管理SecurityContextHolder
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加过滤器

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

}xxxxxxxxxx @EnableMethodSecurity@Configuration@EnableMethodSecuritypublic class SecurityConfig {    @Autowired    private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }    @Bean    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {        http.authorizeHttpRequests((authorizeHttpRequests) -> {                    authorizeHttpRequests.requestMatchers("/swagger-ui/**", "/v3/api-docs/swagger-config", "/v3/api-docs", "/user/login").permitAll() // 放行我自己的登录接口和swagger接口                            .anyRequest().authenticated(); // 其他请求都需要授权后才能使用                })                .csrf(AbstractHttpConfigurer::disable) // 关闭跨域                .sessionManagement(session ->                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)                ) // 不同过session创建管理SecurityContextHolder                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加过滤器        return http.build();    }    @Bean    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {        return authenticationConfiguration.getAuthenticationManager();    }}

SpringSecurity6之前使用@EnableGlobalMethodSecurity(prePostEnabled = true)SpringSecurity6之后这个注解过期了,需要使用@EnableMethodSecurity 其中prePostEnabled 是默认开启的

(2) 在需要校验的接口上添加权限信息

@RestController
@Tag(name = "用户相关接口")
@RequestMapping("/user")
public class UserController {

    @PostMapping
    @Operation(description = "新增用户接口")
    @PreAuthorize("hasAuthority('user:add')")
    public Result<String> addUser(@RequestBody User user) {
        return Result.success();
    }

}

5. 权限认证异常处理

(1) 实现AccessDeniedHandler接口,这个是SpringSecurity处理权限认证的过滤器的接口

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    private Gson gson;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result<String> result = Result.authFailed();
        PrintWriter writer = response.getWriter();
        writer.append(gson.toJson(result));
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

(2) 将处理器注入到SpringSecurity

package com.big.event.common.config;

import com.big.event.common.filter.JWTAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author notch
 * @create 2024-05-24-11:45
 */
@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((authorizeHttpRequests) -> {
                    authorizeHttpRequests.requestMatchers("/swagger-ui/**", "/v3/api-docs/swagger-config", "/v3/api-docs", "/user/login").permitAll() // 放行我自己的登录接口和swagger接口
                            .anyRequest().authenticated(); // 其他请求都需要授权后才能使用
                })
                .csrf(AbstractHttpConfigurer::disable) // 关闭跨域
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                ) // 不同过session创建管理SecurityContextHolder
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) // 添加过滤器
                .exceptionHandling(handler -> handler.accessDeniedHandler(accessDeniedHandler)); // 注入异常处理器

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

}

注意:如果你使用的是注解验证权限,并且配置了Mybatis的全局异常处理器,那么上面的过滤器异常处理器就会失效,使用路径匹配权限则不会

原因:因为使用路径匹配权限,他的验证都是在过滤器中,全局异常处理器是拦截器不会生效,而使用注解验证会在过滤器链结束后进行验证,会在这个拦截器AuthorizationManagerBeforeMethodInterceptor中检查是否有权限,没有会优先被全局异常处理器给拦截,而到不了过滤器中

  • 28
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,目前Spring Security的最新版本是5.5.1,还没有发布6.x版本。不过,我可以为您提供Spring Security 5.x的配置方法。 在Spring Security 5.x中,您可以使用基于Java配置的方式来配置安全性。以下是一个简单的示例: 1.添加Maven依赖: ```xml <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>5.5.1</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.5.1</version> </dependency> ``` 2.创建一个继承自WebSecurityConfigurerAdapter的配置类: ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("{noop}password").roles("USER"); } } ``` 3.在上面的配置类中,我们覆盖了configure(HttpSecurity http)方法,以配置HTTP安全性。在这个例子中,我们允许所有人访问/public/**,但是需要身份验证才能访问其他所有页面。我们还配置了一个自定义的登录页面和一个允许所有人注销的端点。 4.我们还覆盖了configureGlobal(AuthenticationManagerBuilder auth)方法,以配置身份验证。在这个例子中,我们使用了一个内存中的用户存储,用户名为"user",密码为"password",角色为"USER"。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值