使用Spring Boot 3 Security 6.2 JWT 完成无状态的REST接口认证和授权管理。
maven pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.31</version>
</dependency>
</dependencies>
SystemUserEntity
表/实体 用于存放认证需要的基础数据和自己业务的扩展数据
/**
* 模拟你数据库表对象实体
*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class SystemUserEntity {
//id
private String id;
//账号
private String username;
//密码
private String password;
//角色
private List<Integer> roles;
// ----其他你业务需要的字段---
}
PrivilegeEntity
表/实体 用于存放权限数据,一般权限数据在开发时候初始化入库。
/**
* 权限数据库实体模拟
*
*/
@Data
@Builder
public class PrivilegeEntity {
private Integer id;//id
private String name;//名称
private String code;//编码
}
角色表,可以配合Spring Security 的ROLE_XX实现角色管理。一般情况下用的少,一般用细粒度的权限来组合成一个角色而不是直接使用角色。
/**
* 角色模拟数据库实体
*/
@Data
@Builder
public class RoleEntity {
private Integer id;
private String name;//名称
private String code;//编码
private List<PrivilegeEntity> privileges;//角色权限
}
提示:如果需要使用spring security的角色和权限双维度控制,简易角色编码保留ROLE_开头,以便区分角色和权限编码(后续会讲到角色和权限编码会放在一个集合里面“混用”)。
UserService 主要处理用户信息从数据库中拿出来。如用户基础信息/用户角色权限信息
public interface UserService {
/**
* 通过账号找用户
* @param username 账号
* @return 用户
*/
SystemUserEntity findByUsername(String username);
/**
* 角色信息查询
* @param ids
* @return
*/
List<RoleEntity> getRolesById(List<Integer> ids);
/**
* 用户注册模拟
* @param username 账号
* @param password 密码
*/
void register(String username, String password);
/**
* 用户登录,成功后返回token
* @param username
* @param password
* @return
*/
String login(String username, String password);
实现类
@Service
@Slf4j
public class UserServiceImpl implements UserService {
@Resource
PasswordEncoder passwordEncoder;
@Resource
AuthenticationManager authenticationManager;
@Resource
SecurityJwtService securityJwtService;
/**
* 以下部分使用map模拟数据库操作
*
*/
//id->object
final static ConcurrentHashMap<String,SystemUserEntity> database = new ConcurrentHashMap<>();
final static ConcurrentHashMap<Integer, RoleEntity> roles = new ConcurrentHashMap<>();
private static final AtomicInteger roleIdGenerator = new AtomicInteger(0);
/**
* 初始化几个数据
* 实际业务使用数据库动态配置
*/
static {
//权限角色初始化几个
RoleEntity roleUser = RoleEntity.builder()
.id(roleIdGenerator.incrementAndGet())
.code("ROLE_USER")
.privileges(List.of(
PrivilegeEntity.builder().code("user:userInfo").build()
))
.build();
roles.put(roleUser.getId(), roleUser);
RoleEntity roleAdmin = RoleEntity.builder()
.id(roleIdGenerator.incrementAndGet())
.code("ROLE_ADMIN")
.privileges(List.of(
PrivilegeEntity.builder().code("admin:info").build()
))
.build();
roles.put(roleAdmin.getId(), roleAdmin);
//用户初始化几个
SystemUserEntity admin = SystemUserEntity.builder()
.id(UUID.randomUUID().toString())
.username("admin")
//密码123456
.password("$2a$10$U/J6K1YB27tmCQnzUDb9tOh/wlOLzY3aQuiWG7WMCv94gcSSU1fY.")
.roles(List.of(2))
.build();
SystemUserEntity user = SystemUserEntity.builder()
.id(UUID.randomUUID().toString())
.username("user")
//密码123456
.password("$2a$10$U/J6K1YB27tmCQnzUDb9tOh/wlOLzY3aQuiWG7WMCv94gcSSU1fY.")
.roles(List.of(1))
.build();
database.put(admin.getId(), admin);
database.put(user.getId(), user);
}
@Override
public SystemUserEntity findByUsername(String username) {
Optional<Map.Entry<String, SystemUserEntity>> first = database.entrySet().stream().filter(e -> e.getValue().getUsername().equals(username)).findFirst();
return first.map(Map.Entry::getValue).orElse(null);
}
@Override
public List<RoleEntity> getRolesById(List<Integer> ids) {
List<RoleEntity> roleEntityList = new ArrayList<>();
roles.values().stream().filter(o->ids.contains(o.getId())).forEach(roleEntityList::add);
return roleEntityList;
}
@Override
public void register(String username, String password) {
SystemUserEntity byUsername = findByUsername(username);
if (Objects.nonNull(byUsername)){
throw new IllegalArgumentException("账号已经被注册,请换一个");
}
SystemUserEntity build = SystemUserEntity.builder()
.username(username)
.password(passwordEncoder.encode(password))
.id(UUID.fastUUID().toString())
.build();
database.put(build.getId(),build);
}
@Override
public String login(String username, String password) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
SystemUserEntity byUsername = findByUsername(username);
Objects.requireNonNull(byUsername,"账号不存在");
CustomUserDetail userDetail = CustomUserDetail.builder()
.systemUser(byUsername)
.build();
return securityJwtService.generateToken(username);
}
}
提示:这里以map操作代替了数据库操作,简化演示核心学习spring security相关内容,如需数据库相关教程请在站内搜索。
CustomUserDetail
类自定义UserDetails 实现,满足Spring Security载入用户且可以方便扩展登录用户的相关信息
/**
* 注意:通过实现接口UserDetails来自定义登录用户需要定义password字段<br/>
* 通过继续<code>org.springframework.security.core.userdetails.User</code>实现自定义则不需要,或者直接使用该类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CustomUserDetail implements UserDetails {
private SystemUserEntity systemUser;
private List<GrantedAuthority> authorities;
//必须定义该字段,否则启动报错
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null) {
return authorities;
}
return List.of();
}
@Override
public String getPassword() {
return this.systemUser.getPassword();
}
@Override
public String getUsername() {
return this.systemUser.getUsername();
}
//根据业务来是否需要配置,配置则从数据库用户中获取状态返回出来
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
//根据业务来是否需要配置,配置则从数据库用户中获取状态返回出来
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
//根据业务来是否需要配置,配置则从数据库用户中获取状态返回出来
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
//根据业务来是否需要配置,配置则从数据库用户中获取状态返回出来
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}
}
CustomUserDetailsService
类用于实现 Spring Security 框架载入登录用户凭据。通过调用用户的基础业务来获取包括角色信息和权限信息等填充到Spring Security的凭据中。
@Slf4j
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Resource
UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SystemUserEntity byUsername = userService.findByUsername(username);
if (byUsername == null) {
return null;
}
List<GrantedAuthority> authorities = new ArrayList<>();
//找角色/权限
if (CollectionUtil.isNotEmpty(byUsername.getRoles())) {
List<RoleEntity> rolesById = userService.getRolesById(byUsername.getRoles());
List<String> privileges=new ArrayList<>();
for (RoleEntity role : rolesById) {
if (StrUtil.isNotEmpty(role.getCode())){
privileges.add(role.getCode());
}
List<PrivilegeEntity> rolePrivileges = role.getPrivileges();
if (CollectionUtil.isNotEmpty(rolePrivileges)) {
privileges.addAll(rolePrivileges.stream().map(PrivilegeEntity::getCode).toList());
}
}
for (String privilege : privileges) {
authorities.add(new SimpleGrantedAuthority(privilege));
}
}
return CustomUserDetail.builder()
.systemUser(byUsername)
.authorities(authorities)
.build();
}
}
用于提供jwt相关的操作,基于hutool工具,你也可以替换为你自己喜欢的jwt工具
@Component
public class JwtProvider {
//模拟jwt加密密码
final static String SECRET_KEY="123456";
//生成token
public String generateToken(String account) {
Map<String,Object> payload=new HashMap<>();
Date signDate=new Date();
//--------------------------通用数据处理-----------------------
payload.put(JWTPayload.SUBJECT, account);
//签发时间
payload.put(JWTPayload.ISSUED_AT, signDate);
//生效时间
payload.put(JWTPayload.NOT_BEFORE, signDate);
//过期时间 最大有效期1天强制过期
payload.put(JWTPayload.EXPIRES_AT, DateUtil.offsetDay(signDate,1));
//--------------------------通用数据处理-----------------------
//--------------------以下部分可以设置一些自己的数据--------------
payload.put("userId","111");
return JWTUtil.createToken(payload, JWTSignerUtil.hs256(SECRET_KEY.getBytes(StandardCharsets.UTF_8)));
}
public String getLoginUserAccount(HttpServletRequest request) {
String token = getBearerToken(request);
if (StrUtil.isEmpty(token)) {
throw new AuthenticationCredentialsNotFoundException("token is empty !");
}
JWT jwt = verifyToken(token);
if (Objects.isNull(jwt)) {
throw new BadCredentialsException("token invalid !");
}
return jwt.getPayload().getClaimsJson().getStr(JWT.SUBJECT);
}
public String getBearerToken(HttpServletRequest request) {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
return null;
}
//校验签名有效+未过期
public JWT verifyToken(String token) {
JWT jwt = JWTUtil.parseToken(token);
boolean validate = jwt.setKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8)).validate(0);
if (!validate) {
return null;
}
return jwt;
}
}
jwt过滤器在这里主要充当Spring Security认证的逻辑.
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
@Resource
JwtProvider jwtProvider;
@Resource
CustomUserDetailsService customUserDetailsService;
@Resource
AppConfig appConfig;
//此处声明异常全局处理
@Resource
private HandlerExceptionResolver handlerExceptionResolver;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
//忽略地址处理
List<String> ignoreUrls = appConfig.getAuth().getIgnoreUrls();
if (ignoreUrls.stream().anyMatch(e->e.matches(requestURI))) {
filterChain.doFilter(request, response);
return;
}
try {
String loginUserAccount = jwtProvider.getLoginUserAccount(request);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = customUserDetailsService.loadUserByUsername(loginUserAccount);
if (Objects.isNull(userDetails)){
throw new BadCredentialsException("username is invalid");
}
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
context.setAuthentication(authToken);
SecurityContextHolder.setContext(context);
}else {
log.warn("SecurityContextHolder.getContext().getAuthentication() not null");
}
}catch (Exception e){
//丢给自定义全局异常处理器GlobalExceptionHandler,必须手动丢,否则全局异常无法捕获Filter级别的异常
handlerExceptionResolver.resolveException(request, response, null, e);
return;
}
filterChain.doFilter(request, response);
}
}
@Data
public class Result <T>{
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data){
Result<T> result = new Result<T>();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}
public static Result<Void> fail(int code, String msg){
Result<Void> result = new Result<Void>();
result.setCode(code);
result.setMsg(msg);
return result;
}
public static Result<Void> fail(String msg){
Result<Void> result = new Result<Void>();
result.setCode(400);
result.setMsg(msg);
return result;
}
}
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {BindException.class,MethodArgumentNotValidException.class})
public Result<?> methodArgumentNotValidException(Exception e, HttpServletRequest request){
List<FieldError> fieldErrors= new ArrayList<>();
if (e instanceof BindException){
fieldErrors=((BindException) e).getFieldErrors();
}
List<String> errorMsg = new ArrayList<>();
for (FieldError error : fieldErrors) {
errorMsg.add("["+error.getField()+"]"+error.getDefaultMessage());
}
String errMsg = String.join(",", errorMsg);
log.error("请求地址:{},参数校验错误 :{}",request.getRequestURI(),errMsg);
return Result.fail(400, errMsg);
}
@ExceptionHandler({MethodArgumentTypeMismatchException.class,HttpMessageNotReadableException.class})
public Object MethodArgumentTypeMismatchException(Exception e, HttpServletRequest request){
log.error("请求地址:{},参数格式或类型错误:{}",request.getRequestURI(),e.getMessage(),e);
return Result.fail(400,"参数格式或类型错误");
}
@ExceptionHandler({AuthenticationException.class})
public Result<?> authenticationException(AuthenticationException authenticationException,HttpServletRequest request){
log.error("请求地址:{},未登录或令牌无效:{}",request.getRequestURI(),authenticationException.getMessage(),authenticationException);
return Result.fail(401,"未登录或令牌无效");
}
@ExceptionHandler({AccessDeniedException.class})
public Result<?> accessDeniedException(AccessDeniedException e,HttpServletRequest request){
log.error("请求地址:{},未授权访问:{}",request.getRequestURI(),e.getMessage(),e);
return Result.fail(403,"未授权访问");
}
@ExceptionHandler({Exception.class})
public Result<?> exception(Exception e, HttpServletRequest request){
log.error("请求地址:{},未知异常:{}",request.getRequestURI(),e.getMessage(),e);
return Result.fail(500,e.getMessage());
}
}
这里主要关注 401 403的处理。
/**
* <code>@EnableMethodSecurity</code>启用注解权限支持 @Secured / @PreAuthorize<br/>
* <p>
* <code>@Secured("ROLE_ADMIN")</code>和
* <code>@PreAuthorize("hasRole('ROLE_ADMIN')")</code>有角色(两种方式)<br/>
* <code>@PreAuthorize("hasAnyRole({'ROLE_USER','ROLE_ADMIN'})")</code> 有任一角色<br/>
* <code>@PreAuthorize("hasAnyAuthority({'user:search','user:edit'})")</code>有任一权限<br/>
* </p>
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
/**
* 登出处理
*/
@Resource
JwtAuthFilter jwtAuthFilter;
@Resource
CustomUserDetailsService customUserDetailsService;
@Resource
AppConfig appConfig;
/**
*
*/
@Resource
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf(AbstractHttpConfigurer::disable)
// 开启csrf 保护
// .csrf(Customizer.withDefaults())
// 认证失败处理类
// .exceptionHandling(handler->handler.authenticationEntryPoint(new Http403ForbiddenEntryPoint()))
//登初处理REST 可以不处理。也可以自定义一个标记缓存过期
// .logout(logout->logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
.authorizeHttpRequests(authorizeRequests ->{
AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry =
authorizeRequests.requestMatchers("/api/v2/auth/**").permitAll();
if (CollectionUtil.isNotEmpty(appConfig.getAuth().getIgnoreUrls())){
appConfig.getAuth().getIgnoreUrls().forEach(url -> {
registry.requestMatchers(url).permitAll();
});
}
registry.anyRequest().authenticated();
//以下为固定配置参考
// authorizeRequests
// //提示: anonymous permitAll 都表示不需要登录就可以访问。区别在于anonymous匿名访问已经登录则无法访问。
// .requestMatchers("/api/v1/auth/**").permitAll()
// .requestMatchers("/doc.html").permitAll()
// .requestMatchers("/login").permitAll()//不需要验证的
// .requestMatchers("/register").permitAll()
// .requestMatchers("/captcha").permitAll()
// .anyRequest().authenticated();//其他都要认证
}
)
//不通过Session获取SecurityContext
.sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//设定认证过滤器和过滤器顺序在UsernamePasswordAuthenticationFilter前面
//注意:该过滤器需要处理认证相关的逻辑和异常/错误处理,否则spring以匿名登录方式进入,spring security基本就只负责授权相关验证了
.authenticationProvider(authenticationProvider()).addFilterAfter(jwtAuthFilter,UsernamePasswordAuthenticationFilter.class)
;
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(customUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
//忽略处理地址相关配置
@Component
@ConfigurationProperties(prefix = "app")
@Data
public class AppConfig {
@Resource
Auth auth;
@Component
@ConfigurationProperties(prefix = "app.auth")
@Data
public static class Auth{
/**
* 忽略授权地址
*/
private List<String> ignoreUrls = new ArrayList<>();
}
}
application.yml增加配置
app:
auth:
ignore-urls:
- /api/v1/auth/login
- /api/v1/auth/register
前面配置已经完成了整个spring security相关设置,下面是通过UserController 用户登录/注册/获取信息接口进行测试
@RestController
public class UserController {
@Resource
private UserService userService;
@PostMapping("/api/v1/auth/register")
public Result<Void> register(String username, String password) {
userService.register(username, password);
return Result.success(null);
}
@PostMapping("/api/v1/auth/login")
public Result<String> login(String username, String password) {
return Result.success(userService.login(username, password));
}
// @Secured("ROLE_ADMIN")
@PreAuthorize(value = "hasAnyAuthority({'admin:info'})")
@PostMapping("/api/v1/user/getUserInfo")
public Result<SystemUserEntity> getUserInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Objects.requireNonNull(authentication,"authentication is null");
CustomUserDetail customUserDetail = (CustomUserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SystemUserEntity byUsername = userService.findByUsername(customUserDetail.getUsername());
return Result.success(byUsername);
}
}
测试预期结果:输入错误的账户密码提示错误/ admin/111111
测试预期结果:输入正确的账户密码返回登录成功的token值 admin/123456
测试结果:正常情况和错误情况均与预期结果一致,测试通过。
认证访问测试即:接口只要是登录了都能访问。
首先把测试接口调整成下面的样子(编辑代码后均需重启idea)
代码聚焦
// @Secured("ROLE_ADMIN")
// @PreAuthorize(value = "hasAnyAuthority({'admin:info'})")
@PostMapping("/api/v1/user/getUserInfo")
public Result<SystemUserEntity> getUserInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Objects.requireNonNull(authentication,"authentication is null");
CustomUserDetail customUserDetail = (CustomUserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SystemUserEntity byUsername = userService.findByUsername(customUserDetail.getUsername());
return Result.success(byUsername);
}
去掉所有权限注解,访问该接口仅需登录即可
测试预期结果:未携带token或token问题,返回401 未认证
测试预期结果:携带正确的token接口正常返回数据
测试结果:未携带token和携带token执行结果与预期一致,测试通过。
授权访问是在认证的基础上添加了角色或者权限的校验,需满足角色或权限校验结果才能正常访问,否则返回403未授权
编辑测试接口,需要ROLE_ADMIN才能访问(编辑代码后均需重启idea)
@Secured("ROLE_ADMIN")
// @PreAuthorize(value = "hasAnyAuthority({'admin:info'})")
@PostMapping("/api/v1/user/getUserInfo")
public Result<SystemUserEntity> getUserInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Objects.requireNonNull(authentication,"authentication is null");
CustomUserDetail customUserDetail = (CustomUserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SystemUserEntity byUsername = userService.findByUsername(customUserDetail.getUsername());
return Result.success(byUsername);
}
提示:前面我们已经初始化了两个用户user和admin
测试预期结果:使用用户user/123456 token访问该接口返回403
测试预期结果:使用用户admin/123456 token访问该接口返回正常数据
测试结果:user账户和admin账户测试结果均与预期结果一致。
编辑接口,设定admin:info权限访问 (编辑代码后均需重启idea)
代码聚焦
// @Secured("ROLE_ADMIN")
@PreAuthorize(value = "hasAnyAuthority({'admin:info'})")
@PostMapping("/api/v1/user/getUserInfo")
public Result<SystemUserEntity> getUserInfo() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Objects.requireNonNull(authentication,"authentication is null");
CustomUserDetail customUserDetail = (CustomUserDetail) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SystemUserEntity byUsername = userService.findByUsername(customUserDetail.getUsername());
return Result.success(byUsername);
}
测试预期结果:使用user账户访问返回403(user用户没有这个权限编码)
测试预期结果:使用admin账户访问成功返回数据(admin用户的权限有这个编码)
测试结果:user用户和admin用户测试结果均与预期一致,测试通过。
https://www.leftso.com/article/2408191444549714.html