前言
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 {@Serialprivate 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 setterspublic 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 {@Resourceprivate MyAuthenticationEntryPoint myAuthenticationEntryPoint;@Resourceprivate UserDetailsService customUserDetailsService;@Resourceprivate PasswordEncoder passwordEncoder;@Resourceprivate TokenAuthenticationFilter tokenAuthenticationFilter;@Beanpublic MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider() {MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider = new MobilecodeAuthenticationProvider();mobilecodeAuthenticationProvider.setUserDetailsService(customUserDetailsService);return mobilecodeAuthenticationProvider;}@Beanpublic DaoAuthenticationProvider daoAuthenticationProvider() {DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);daoAuthenticationProvider.setUserDetailsService(customUserDetailsService);daoAuthenticationProvider.setHideUserNotFoundExceptions(false);return daoAuthenticationProvider;}/*** 定义认证管理器AuthenticationManager* @return AuthenticationManager*/@Beanpublic 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;@Overridepublic 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());}@Overridepublic 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 {@Resourceprivate 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);}@Overridepublic 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 {@Overridepublic 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));}@Overridepublic 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 {@Overrideprotected 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 密码编码器*/@Beanpublic 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 {@Resourceprivate 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做个完善。
作为记录的同时也希望能帮助到需要的朋友。
创作不易,欢迎一键三连。