前言
springboot3已经推出有一段时间了,近期公司里面的小项目使用的都是springboot3版本的,安全框架还是以springsecurity为主,毕竟亲生的。
本文针对基于springboot3和springsecurity实现用户登录认证访问以及异常处理做个记录总结,也希望能帮助到需要的朋友。
目标
- 需要提供登录接口,支持用户名+密码和手机号+验证码两种方式,当然后续可以根据实际需要进行扩展
- 登录成功后返回一个token用于后续接口访问凭证
- 请求时如果是需要校验认证的接口没有传递指定请求头返回401
- 请求时如果用户权限不足,返回403
- 如果认证通过且权限满足,正常返回数据
准备工作
1. 新建项目
pom.xml (供参考)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zjtx.tech.security</groupId>
<artifactId>security_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
</parent>
<properties>
<maven.compiler.source>20</maven.compiler.source>
<maven.compiler.target>20</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
相对来说比较简单:
- 引入了
spring-boot-starter-web
- 引入了
spring-boot-starter-security
- 引入了
lombok
注意:
- springboot3要求使用的jdk版本在17+,本文使用的是openjdk20版本,springboot使用的是3.1.2版本。
- 我们第一个版本先采用模拟数据实现功能,后续再补充实际逻辑,再根据需要调整pom文件
2. 准备基础类
主要包含统一响应、统一异常处理、自定义异常类等。
统一响应类-Result.java
package com.zjtx.tech.security.demo.common;
import java.io.Serial;
import java.io.Serializable;
public class Result<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
// 状态码
private int code;
// 消息描述
private String msg;
// 数据内容
private T data;
public Result() {}
public Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
// 成功响应构造器
public static <T> Result<T> ok(T data) {
return new Result<>(200, "success", data);
}
// 失败响应构造器
public static <T> Result<T> fail(int code, String msg) {
return new Result<>(code, msg, null);
}
// 错误响应构造器
public static <T> Result<T> error(String errorMessage) {
return new Result<>(500, errorMessage, null);
}
// getters and setters
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
JSON转换工具类-JsonUtil.java
package com.zjtx.tech.security.demo.util;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
public class JsonUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 将Java对象转换为JSON字符串
* @param obj 需要转换的Java对象
* @return JSON格式的字符串
*/
public static String toJson(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to convert object to JSON", e);
}
}
/**
* 将JSON字符串转换为指定类型的Java对象
* @param jsonStr JSON格式的字符串
* @param clazz 目标对象的Class类型
* @param <T> 泛型类型
* @return 转换后的Java对象实例
*/
public static <T> T toObject(String jsonStr, Class<T> clazz) {
try {
return objectMapper.readValue(jsonStr, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to convert JSON string to object", e);
}
}
/**
* 将JSON字符串转换为指定类型的Java List对象
* @param jsonStr JSON格式的字符串
* @param elementType 列表中元素的Class类型
* @param <T> 泛型类型
* @return 转换后的Java List对象实例
*/
public static <T> List<T> jsonToList(String jsonStr, Class<T> elementType) {
try {
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, elementType);
return objectMapper.readValue(jsonStr, javaType);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to convert JSON string to list", e);
}
}
}
自定义异常类-AuthorizationExceptionEx.java
package com.zjtx.tech.security.demo.exceptions;
import org.springframework.security.core.AuthenticationException;
public class AuthorizationExceptionEx extends AuthenticationException {
public AuthorizationExceptionEx(String msg, Throwable cause) {
super(msg, cause);
}
public AuthorizationExceptionEx(String msg) {
super(msg);
}
}
自定义异常类-ServerException.java
package com.zjtx.tech.security.demo.exceptions;
public class ServerException extends RuntimeException {
public ServerException(String message) {
super(message);
}
public ServerException(String message, Throwable cause) {
super(message, cause);
}
}
全局异常捕获处理-GlobalExceptionHandler.java
package com.zjtx.tech.security.demo.exceptions;
import com.zjtx.tech.security.demo.common.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthorizationExceptionEx.class)
public Result<String> authorizationExceptionHandling(AuthorizationExceptionEx ex) {
System.out.println("authorizationExceptionHandling = " + ex);
return Result.fail(1000, ex.getMessage());
}
// handling specific exception
@ExceptionHandler(ServerException.class)
public Result<String> serverExceptionHandling(ServerException ex) {
System.out.println("serverExceptionHandling = " + ex);
return Result.fail(6000, ex.getMessage());
}
@ExceptionHandler(AccessDeniedException.class)
public Result<String> accessDeniedExceptionHandling(AccessDeniedException ex) {
System.out.println("accessDeniedExceptionHandling = " + ex);
return Result.fail(403, "权限不足");
}
// handling global exception
@ExceptionHandler(Exception.class)
public Result<String> exceptionHandling(Exception ex) {
System.out.println("exceptionHandling = " + ex);
return Result.fail(500, "服务器内部异常,请稍后重试");
}
}
开始
编写springsecurity配置类
MySecurityConfigurer.java
package com.zjtx.tech.security.demo.config;
import com.zjtx.tech.security.demo.provider.MobilecodeAuthenticationProvider;
import com.zjtx.tech.security.demo.provider.MyAuthenticationEntryPoint;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class MySecurityConfigurer {
@Resource
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Resource
private UserDetailsService customUserDetailsService;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private TokenAuthenticationFilter tokenAuthenticationFilter;
@Bean
public MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider() {
MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider = new MobilecodeAuthenticationProvider();
mobilecodeAuthenticationProvider.setUserDetailsService(customUserDetailsService);
return mobilecodeAuthenticationProvider;
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
daoAuthenticationProvider.setUserDetailsService(customUserDetailsService);
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
return daoAuthenticationProvider;
}
/**
* 定义认证管理器AuthenticationManager
* @return AuthenticationManager
*/
@Bean
public AuthenticationManager authenticationManager() {
List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
authenticationProviders.add(mobilecodeAuthenticationProvider());
authenticationProviders.add(daoAuthenticationProvider());
return new ProviderManager(authenticationProviders);
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) ->
authorize.requestMatchers(new AntPathRequestMatcher("/login/**")).permitAll()
.anyRequest().authenticated())
.cors(Customizer.withDefaults())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(configure -> {
configure.authenticationEntryPoint(myAuthenticationEntryPoint);
})
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
}
这个类是springsecurity的统一配置类,不仅包含了AuthorizationProvider
这个关键认证bean的定义,同时还定义了访问策略以及异常处理策略等信息。其中使用了springsecurity6中相对较新的语法,参考价值相对较高。
里面涉及到几个关键的bean,如下:
MyAuthenticationEntryPoint
自定义的异常处理类,用于处理认证异常及访问被拒绝异常UserDetailsService
springsecurity提供的获取用户信息的一个接口,需要使用者自行完善PasswordEncoder
密码加密方法类,由使用者自行扩展TokenAuthenticationFilter
自定义的请求token校验过滤器MobilecodeAuthenticationProvider
手机号验证码身份源 用于校验用户手机号和验证码相关信息,实现可参考Springsecurity自带的DaoAuthorizationProvider.java
类
上面这些关键类我们接下来都会一一给出示例代码。
编写身份认证源类
MobilecodeAuthenticationProvider.java
package com.zjtx.tech.security.demo.provider;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.HashMap;
import java.util.Map;
public class MobilecodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
MobilecodeAuthenticationToken mobilecodeAuthenticationToken = (MobilecodeAuthenticationToken) authentication;
String phone = mobilecodeAuthenticationToken.getPhone();
String mobileCode = mobilecodeAuthenticationToken.getMobileCode();
System.out.println("登陆手机号:" + phone);
System.out.println("手机验证码:" + mobileCode);
// 模拟从redis中读取手机号对应的验证码及其用户名
Map<String, String> dataFromRedis = new HashMap<>();
dataFromRedis.put("code", "6789");
dataFromRedis.put("username", "admin");
// 判断验证码是否一致
if (!mobileCode.equals(dataFromRedis.get("code"))) {
throw new BadCredentialsException("验证码错误");
}
// 如果验证码一致,从数据库中读取该手机号对应的用户信息
CustomUserDetails loadedUser = (CustomUserDetails) userDetailsService.loadUserByUsername(dataFromRedis.get("username"));
if (loadedUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new MobilecodeAuthenticationToken(loadedUser, null, loadedUser.getAuthorities());
}
@Override
public boolean supports(Class<?> aClass) {
return MobilecodeAuthenticationToken.class.isAssignableFrom(aClass);
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
说明如下:
上面类中比较关键的就是
authenticate
和support
方法,如果看过一点源码的话可以知道这里会存在多个Provider,通过support方法来确定使用哪个Provider的实现类。
authenticate
就是具体的认证逻辑,如判断验证码是否正确,根据手机号查找用户信息等。
authenticate
方法中的参数就是在用户登录时组装和传递进来的。
其中涉及到UserDetailService
的实现类如下:
package com.zjtx.tech.security.demo.provider;
import jakarta.annotation.Resource;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
private static final Collection<GrantedAuthority> authorities = new ArrayList<>();
static {
GrantedAuthority defaultRole = new SimpleGrantedAuthority("common");
GrantedAuthority xxlJobRole = new SimpleGrantedAuthority("xxl-job");
authorities.add(defaultRole);
authorities.add(xxlJobRole);
}
@Override
public UserDetails loadUserByUsername(String username) throws AuthenticationException {
CustomUserDetails userDetails;
// 这里模拟从数据库中获取用户信息
if (username.equals("admin")) {
//这里的admin用户拥有common和xxl-job两个权限
userDetails = new CustomUserDetails("admin", passwordEncoder.encode("123456"), authorities);
userDetails.setAge(25);
userDetails.setSex(1);
userDetails.setAddress("xxxx小区");
return userDetails;
} else {
throw new UsernameNotFoundException("用户不存在");
}
}
}
目前这个类中采用的是模拟数据,后续我们会在这个基础上接入真实数据及实现。
还涉及到MobilecodeAuthenticationToken.java
这个类,如下:
package com.zjtx.tech.security.demo.provider;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 手机验证码认证信息,在UsernamePasswordAuthenticationToken的基础上添加属性 手机号、验证码
*/
public class MobilecodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 530L;
private Object principal;
private Object credentials;
private String phone;
private String mobileCode;
public MobilecodeAuthenticationToken(String phone, String mobileCode) {
super(null);
this.phone = phone;
this.mobileCode = mobileCode;
this.setAuthenticated(false);
}
public MobilecodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
public String getPhone() {
return phone;
}
public String getMobileCode() {
return mobileCode;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
涉及到的用户信息类如下:
package com.zjtx.tech.security.demo.provider;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
import java.util.List;
public class CustomUserDetails extends User {
private int age;
private int sex;
private String address;
private String phone;
private List<String> roles;
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
继承了org.springframework.security.core.userdetails.User
这个类同时添加了一些自定义属性,可自行扩展。
编写认证异常处理类
上面在安全配置类中用到了这个异常处理类,主要处理认证异常和访问被拒绝。
MyAuthenticationEntryPoint.java
package com.zjtx.tech.security.demo.provider;
import com.zjtx.tech.security.demo.common.Result;
import com.zjtx.tech.security.demo.util.JsonUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint, AccessDeniedHandler {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
Result<String> result = Result.fail(401, "用户未登录或已过期");
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JsonUtil.toJson(result));
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result<String> result = Result.fail(403, "权限不足");
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JsonUtil.toJson(result));
}
}
比较简单,实现了两个接口,返回不同的json数据。JsonUtil比较简单,就不在此列出了。
编写认证过滤器类
过滤器在认证中扮演者非常重要的角色,我们也定义了一个用于token校验的filter,如下:
package com.zjtx.tech.security.demo.config;
import com.zjtx.tech.security.demo.provider.CustomUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
@WebFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(@NonNull HttpServletRequest servletRequest, @NonNull HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException, ServletException {
String token = getToken(servletRequest);
// 如果没有token,跳过该过滤器
if (StringUtils.hasText(token)) {
// 模拟redis中的数据
Map<String, CustomUserDetails> map = new HashMap<>();
//这里放入了两个示例token 仅供测试
map.put("test_token1", new CustomUserDetails("admin", new BCryptPasswordEncoder().encode("123456"), AuthorityUtils.createAuthorityList("common", "xxl-job")));
map.put("test_token2", new CustomUserDetails("root", new BCryptPasswordEncoder().encode("123456"), AuthorityUtils.createAuthorityList("common")));
// 这里模拟从redis获取token对应的用户信息
CustomUserDetails customUserDetail = map.get(token);
if (customUserDetail != null) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(customUserDetail, null, customUserDetail.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
}
filterChain.doFilter(servletRequest, httpServletResponse);
}
/**
* 从请求中获取token
* @param servletRequest 请求对象
* @return 获取到的token值 可以为null
*/
private String getToken(HttpServletRequest servletRequest) {
//先从请求头中获取
String headerToken = servletRequest.getHeader("Authorization");
if(StringUtils.hasText(headerToken)) {
return headerToken;
}
//再从请求参数里获取
String paramToken = servletRequest.getParameter("accessToken");
if(StringUtils.hasText(paramToken)) {
return paramToken;
}
return null;
}
}
主要完成的工作就是从请求头或者请求参数中获取token,与redis或其他存储介质中的进行比对,如果存在对应用户则正常访问,否则执行其他策略或者抛出异常。
这里内置了两个token,分别拥有不同权限。
编写密码加密及比对器
springsecurity中提供了一个PasswordEncoder
接口,用于对密码进行加密和比对,我们也定义这样一个bean
package com.zjtx.tech.security.demo.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class PasswordEncoderConfig {
/**
* 获取密码编码方式
*/
@Value("${password.encode.key:bcrypt}")
private String passwordEncodeKey;
/**
* 获取密码编码器
* @return 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", new SCryptPasswordEncoder(4,8, 1,32, 16));
return new DelegatingPasswordEncoder(passwordEncodeKey, encoders);
}
}
这里采用的实现类是DelegatingPasswordEncoder
,一个好处是它可以兼容多种加密方式,区分的办法是根据加密后的字符串前缀,如bcrypt加密后的结果前缀就是{bcrypt},方便配置和扩展,不做过多阐述。
编写测试和登录用的controller
登录接口
package com.zjtx.tech.security.demo.controller;
import com.zjtx.tech.security.demo.common.Result;
import com.zjtx.tech.security.demo.provider.MobilecodeAuthenticationToken;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
private AuthenticationManager authenticationManager;
/**
* 用户名密码登录
* @param username 用户名
* @param password 密码
* @return 返回登录结果
*/
@GetMapping("/usernamePwd")
public Result<?> usernamePwd(String username, String password) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
try {
authenticationManager.authenticate(usernamePasswordAuthenticationToken);
} catch (BadCredentialsException | UsernameNotFoundException e) {
throw new ServerException(e.getMessage());
}
String token = UUID.randomUUID().toString().replace("-", "");
return Result.ok(token);
}
/**
* 手机验证码登录
* @param phone 手机号
* @param mobileCode 验证码
* @return 返回登录结果
*/
@GetMapping("/mobileCode")
public Result<?> mobileCode(String phone, String mobileCode) {
MobilecodeAuthenticationToken mobilecodeAuthenticationToken = new MobilecodeAuthenticationToken(phone, mobileCode);
Authentication authenticate;
try {
authenticate = authenticationManager.authenticate(mobilecodeAuthenticationToken);
} catch (Exception e) {
e.printStackTrace();
return Result.error("验证码错误");
}
System.out.println(authenticate);
String token = UUID.randomUUID().toString().replace("-", "");
return Result.ok(token);
}
}
可以看到这个controller提供了用户名+密码登录和手机号+验证码登录两个接口。
测试用的接口:
package com.zjtx.tech.security.demo.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("demo")
@PreAuthorize("hasAuthority('xxl-job')")
public String demo(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("authentication = " + authentication);
return "hello world";
}
}
这个controller定义了一个方法,这个方法需要用户拥有xxl-job的权限。
期望结果
结合我们之前定义的一些类,猜测期望结果应该是这样的:
- 使用用户名+密码登录时 如果是admin + 123456 可以正常登录 其他提示6000 登录失败
- 使用手机号+验证码登录时 如果是xxx + 6789 可以正常登录 其他提示6000 验证码错误
- 使用Authorization: test_token1访问demo接口时用户拥有common和xxl-job权限,可以正常访问demo接口
- 使用Authorization: test_token2访问demo接口时用户拥有common权限,访问demo接口时提示403 权限不足
- 使用其他token访问demo接口时提示401 用户未登录或token已过期
验证
启动项目,默认端口8080,使用postman模拟请求进行简单测试。
- 验证登录
- 验证接口访问
结论
经验证,结果符合预期
。
总结
本文中我们完成了基于springboot3+springsecurity实现用户认证登录及鉴权访问的简单demo, 接下来我们会继续把获取及验证用户、生成token、校验token做个完善。
作为记录的同时也希望能帮助到需要的朋友。
创作不易,欢迎一键三连。