认证
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
中检查是否有权限,没有会优先被全局异常处理器给拦截,而到不了过滤器中