0


【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)

前言

springboot3已经推出有一段时间了,近期公司里面的小项目使用的都是springboot3版本的,安全框架还是以springsecurity为主,毕竟亲生的。

本文针对基于springboot3和springsecurity实现用户登录认证访问以及异常处理做个记录总结,也希望能帮助到需要的朋友。

目标

  1. 需要提供登录接口,支持用户名+密码和手机号+验证码两种方式,当然后续可以根据实际需要进行扩展
  2. 登录成功后返回一个token用于后续接口访问凭证
  3. 请求时如果是需要校验认证的接口没有传递指定请求头返回401
  4. 请求时如果用户权限不足,返回403
  5. 如果认证通过且权限满足,正常返回数据

准备工作

1. 新建项目

pom.xml (供参考)

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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>

相对来说比较简单:

  1. 引入了spring-boot-starter-web
  2. 引入了spring-boot-starter-security
  3. 引入了lombok

注意:

  1. springboot3要求使用的jdk版本在17+,本文使用的是openjdk20版本,springboot使用的是3.1.2版本。
  2. 我们第一个版本先采用模拟数据实现功能,后续再补充实际逻辑,再根据需要调整pom文件

2. 准备基础类

主要包含统一响应、统一异常处理、自定义异常类等。

统一响应类-Result.java

packagecom.zjtx.tech.security.demo.common;importjava.io.Serial;importjava.io.Serializable;publicclassResult<T>implementsSerializable{@Serialprivatestaticfinallong serialVersionUID =1L;// 状态码privateint code;// 消息描述privateString msg;// 数据内容privateT data;publicResult(){}publicResult(int code,String msg,T data){this.code = code;this.msg = msg;this.data = data;}// 成功响应构造器publicstatic<T>Result<T>ok(T data){returnnewResult<>(200,"success", data);}// 失败响应构造器publicstatic<T>Result<T>fail(int code,String msg){returnnewResult<>(code, msg,null);}// 错误响应构造器publicstatic<T>Result<T>error(String errorMessage){returnnewResult<>(500, errorMessage,null);}// getters and setterspublicintgetCode(){return code;}publicvoidsetCode(int code){this.code = code;}publicStringgetMsg(){return msg;}publicvoidsetMsg(String msg){this.msg = msg;}publicTgetData(){return data;}publicvoidsetData(T data){this.data = data;}}

JSON转换工具类-JsonUtil.java

packagecom.zjtx.tech.security.demo.util;importcom.fasterxml.jackson.core.JsonProcessingException;importcom.fasterxml.jackson.databind.JavaType;importcom.fasterxml.jackson.databind.ObjectMapper;importjava.util.List;publicclassJsonUtil{privatestaticfinalObjectMapper objectMapper =newObjectMapper();/**
     * 将Java对象转换为JSON字符串
     * @param obj 需要转换的Java对象
     * @return JSON格式的字符串
     */publicstaticStringtoJson(Object obj){try{return objectMapper.writeValueAsString(obj);}catch(JsonProcessingException e){thrownewRuntimeException("Failed to convert object to JSON", e);}}/**
     * 将JSON字符串转换为指定类型的Java对象
     * @param jsonStr JSON格式的字符串
     * @param clazz 目标对象的Class类型
     * @param <T> 泛型类型
     * @return 转换后的Java对象实例
     */publicstatic<T>TtoObject(String jsonStr,Class<T> clazz){try{return objectMapper.readValue(jsonStr, clazz);}catch(JsonProcessingException e){thrownewRuntimeException("Failed to convert JSON string to object", e);}}/**
     * 将JSON字符串转换为指定类型的Java List对象
     * @param jsonStr JSON格式的字符串
     * @param elementType 列表中元素的Class类型
     * @param <T> 泛型类型
     * @return 转换后的Java List对象实例
     */publicstatic<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){thrownewRuntimeException("Failed to convert JSON string to list", e);}}}

自定义异常类-AuthorizationExceptionEx.java

packagecom.zjtx.tech.security.demo.exceptions;importorg.springframework.security.core.AuthenticationException;publicclassAuthorizationExceptionExextendsAuthenticationException{publicAuthorizationExceptionEx(String msg,Throwable cause){super(msg, cause);}publicAuthorizationExceptionEx(String msg){super(msg);}}

自定义异常类-ServerException.java

packagecom.zjtx.tech.security.demo.exceptions;publicclassServerExceptionextendsRuntimeException{publicServerException(String message){super(message);}publicServerException(String message,Throwable cause){super(message, cause);}}

全局异常捕获处理-GlobalExceptionHandler.java

packagecom.zjtx.tech.security.demo.exceptions;importcom.zjtx.tech.security.demo.common.Result;importorg.springframework.security.access.AccessDeniedException;importorg.springframework.web.bind.annotation.ExceptionHandler;importorg.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvicepublicclassGlobalExceptionHandler{@ExceptionHandler(AuthorizationExceptionEx.class)publicResult<String>authorizationExceptionHandling(AuthorizationExceptionEx ex){System.out.println("authorizationExceptionHandling = "+ ex);returnResult.fail(1000, ex.getMessage());}// handling specific exception@ExceptionHandler(ServerException.class)publicResult<String>serverExceptionHandling(ServerException ex){System.out.println("serverExceptionHandling = "+ ex);returnResult.fail(6000, ex.getMessage());}@ExceptionHandler(AccessDeniedException.class)publicResult<String>accessDeniedExceptionHandling(AccessDeniedException ex){System.out.println("accessDeniedExceptionHandling = "+ ex);returnResult.fail(403,"权限不足");}// handling global exception@ExceptionHandler(Exception.class)publicResult<String>exceptionHandling(Exception ex){System.out.println("exceptionHandling = "+ ex);returnResult.fail(500,"服务器内部异常,请稍后重试");}}

开始

编写springsecurity配置类

MySecurityConfigurer.java

packagecom.zjtx.tech.security.demo.config;importcom.zjtx.tech.security.demo.provider.MobilecodeAuthenticationProvider;importcom.zjtx.tech.security.demo.provider.MyAuthenticationEntryPoint;importjakarta.annotation.Resource;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.annotation.Order;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.authentication.AuthenticationProvider;importorg.springframework.security.authentication.ProviderManager;importorg.springframework.security.authentication.dao.DaoAuthenticationProvider;importorg.springframework.security.config.Customizer;importorg.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;importorg.springframework.security.config.http.SessionCreationPolicy;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.DelegatingPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.crypto.password.Pbkdf2PasswordEncoder;importorg.springframework.security.crypto.scrypt.SCryptPasswordEncoder;importorg.springframework.security.web.SecurityFilterChain;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;importorg.springframework.security.web.util.matcher.AntPathRequestMatcher;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;@EnableMethodSecurity@EnableWebSecurity@ConfigurationpublicclassMySecurityConfigurer{@ResourceprivateMyAuthenticationEntryPoint myAuthenticationEntryPoint;@ResourceprivateUserDetailsService customUserDetailsService;@ResourceprivatePasswordEncoder passwordEncoder;@ResourceprivateTokenAuthenticationFilter tokenAuthenticationFilter;@BeanpublicMobilecodeAuthenticationProvidermobilecodeAuthenticationProvider(){MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider =newMobilecodeAuthenticationProvider();
        mobilecodeAuthenticationProvider.setUserDetailsService(customUserDetailsService);return mobilecodeAuthenticationProvider;}@BeanpublicDaoAuthenticationProviderdaoAuthenticationProvider(){DaoAuthenticationProvider daoAuthenticationProvider =newDaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        daoAuthenticationProvider.setUserDetailsService(customUserDetailsService);
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);return daoAuthenticationProvider;}/**
     * 定义认证管理器AuthenticationManager
     * @return AuthenticationManager
     */@BeanpublicAuthenticationManagerauthenticationManager(){List<AuthenticationProvider> authenticationProviders =newArrayList<>();
        authenticationProviders.add(mobilecodeAuthenticationProvider());
        authenticationProviders.add(daoAuthenticationProvider());returnnewProviderManager(authenticationProviders);}@Bean@Order(2)publicSecurityFilterChaindefaultSecurityFilterChain(HttpSecurity http)throwsException{
        http
                .authorizeHttpRequests((authorize)->
                        authorize.requestMatchers(newAntPathRequestMatcher("/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,如下:

  1. MyAuthenticationEntryPoint 自定义的异常处理类,用于处理认证异常及访问被拒绝异常
  2. UserDetailsService springsecurity提供的获取用户信息的一个接口,需要使用者自行完善
  3. PasswordEncoder 密码加密方法类,由使用者自行扩展
  4. TokenAuthenticationFilter 自定义的请求token校验过滤器
  5. MobilecodeAuthenticationProvider 手机号验证码身份源 用于校验用户手机号和验证码相关信息,实现可参考Springsecurity自带的DaoAuthorizationProvider.java

上面这些关键类我们接下来都会一一给出示例代码。

编写身份认证源类

MobilecodeAuthenticationProvider.java

packagecom.zjtx.tech.security.demo.provider;importorg.springframework.security.authentication.AuthenticationProvider;importorg.springframework.security.authentication.BadCredentialsException;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importjava.util.HashMap;importjava.util.Map;publicclassMobilecodeAuthenticationProviderimplementsAuthenticationProvider{privateUserDetailsService userDetailsService;@OverridepublicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{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 =newHashMap<>();
        dataFromRedis.put("code","6789");
        dataFromRedis.put("username","admin");// 判断验证码是否一致if(!mobileCode.equals(dataFromRedis.get("code"))){thrownewBadCredentialsException("验证码错误");}// 如果验证码一致,从数据库中读取该手机号对应的用户信息CustomUserDetails loadedUser =(CustomUserDetails) userDetailsService.loadUserByUsername(dataFromRedis.get("username"));if(loadedUser ==null){thrownewUsernameNotFoundException("用户不存在");}returnnewMobilecodeAuthenticationToken(loadedUser,null, loadedUser.getAuthorities());}@Overridepublicbooleansupports(Class<?> aClass){returnMobilecodeAuthenticationToken.class.isAssignableFrom(aClass);}publicvoidsetUserDetailsService(UserDetailsService userDetailsService){this.userDetailsService = userDetailsService;}}

说明如下:

  1. 上面类中比较关键的就是authenticatesupport方法,如果看过一点源码的话可以知道这里会存在多个Provider,通过support方法来确定使用哪个Provider的实现类。
  2. authenticate就是具体的认证逻辑,如判断验证码是否正确,根据手机号查找用户信息等。
  3. authenticate方法中的参数就是在用户登录时组装和传递进来的。

其中涉及到

UserDetailService

的实现类如下:

packagecom.zjtx.tech.security.demo.provider;importjakarta.annotation.Resource;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.stereotype.Service;importjava.util.ArrayList;importjava.util.Collection;@ServicepublicclassMyUserDetailsServiceimplementsUserDetailsService{@ResourceprivatePasswordEncoder passwordEncoder;privatestaticfinalCollection<GrantedAuthority> authorities =newArrayList<>();static{GrantedAuthority defaultRole =newSimpleGrantedAuthority("common");GrantedAuthority xxlJobRole =newSimpleGrantedAuthority("xxl-job");
        authorities.add(defaultRole);
        authorities.add(xxlJobRole);}@OverridepublicUserDetailsloadUserByUsername(String username)throwsAuthenticationException{CustomUserDetails userDetails;// 这里模拟从数据库中获取用户信息if(username.equals("admin")){//这里的admin用户拥有common和xxl-job两个权限
            userDetails =newCustomUserDetails("admin", passwordEncoder.encode("123456"), authorities);
            userDetails.setAge(25);
            userDetails.setSex(1);
            userDetails.setAddress("xxxx小区");return userDetails;}else{thrownewUsernameNotFoundException("用户不存在");}}}

目前这个类中采用的是模拟数据,后续我们会在这个基础上接入真实数据及实现。

还涉及到

MobilecodeAuthenticationToken.java

这个类,如下:

packagecom.zjtx.tech.security.demo.provider;importorg.springframework.security.authentication.AbstractAuthenticationToken;importorg.springframework.security.core.GrantedAuthority;importjava.util.Collection;/**
 * 手机验证码认证信息,在UsernamePasswordAuthenticationToken的基础上添加属性 手机号、验证码
 */publicclassMobilecodeAuthenticationTokenextendsAbstractAuthenticationToken{privatestaticfinallong serialVersionUID =530L;privateObject principal;privateObject credentials;privateString phone;privateString mobileCode;publicMobilecodeAuthenticationToken(String phone,String mobileCode){super(null);this.phone = phone;this.mobileCode = mobileCode;this.setAuthenticated(false);}publicMobilecodeAuthenticationToken(Object principal,Object credentials,Collection<?extendsGrantedAuthority> authorities){super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true);}publicObjectgetCredentials(){returnthis.credentials;}publicObjectgetPrincipal(){returnthis.principal;}publicStringgetPhone(){return phone;}publicStringgetMobileCode(){return mobileCode;}publicvoidsetAuthenticated(boolean isAuthenticated)throwsIllegalArgumentException{if(isAuthenticated){thrownewIllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}else{super.setAuthenticated(false);}}publicvoideraseCredentials(){super.eraseCredentials();this.credentials =null;}}

涉及到的用户信息类如下:

packagecom.zjtx.tech.security.demo.provider;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.userdetails.User;importjava.util.Collection;importjava.util.List;publicclassCustomUserDetailsextendsUser{privateint age;privateint sex;privateString address;privateString phone;privateList<String> roles;publicCustomUserDetails(String username,String password,Collection<?extendsGrantedAuthority> authorities){super(username, password, authorities);}publicCustomUserDetails(String username,String password,boolean enabled,boolean accountNonExpired,boolean credentialsNonExpired,boolean accountNonLocked,Collection<?extendsGrantedAuthority> authorities){super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);}publicintgetAge(){return age;}publicvoidsetAge(int age){this.age = age;}publicintgetSex(){return sex;}publicvoidsetSex(int sex){this.sex = sex;}publicStringgetAddress(){return address;}publicvoidsetAddress(String address){this.address = address;}publicStringgetPhone(){return phone;}publicvoidsetPhone(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,如下:

packagecom.zjtx.tech.security.demo.config;importcom.zjtx.tech.security.demo.provider.CustomUserDetails;importjakarta.servlet.FilterChain;importjakarta.servlet.ServletException;importjakarta.servlet.annotation.WebFilter;importjakarta.servlet.http.HttpServletRequest;importjakarta.servlet.http.HttpServletResponse;importorg.springframework.lang.NonNull;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.authority.AuthorityUtils;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.stereotype.Component;importorg.springframework.util.StringUtils;importorg.springframework.web.filter.OncePerRequestFilter;importjava.io.IOException;importjava.util.HashMap;importjava.util.Map;@Component@WebFilterpublicclassTokenAuthenticationFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(@NonNullHttpServletRequest servletRequest,@NonNullHttpServletResponse httpServletResponse,FilterChain filterChain)throwsServletException,IOException,ServletException{String token =getToken(servletRequest);// 如果没有token,跳过该过滤器if(StringUtils.hasText(token)){// 模拟redis中的数据Map<String,CustomUserDetails> map =newHashMap<>();//这里放入了两个示例token 仅供测试
            map.put("test_token1",newCustomUserDetails("admin",newBCryptPasswordEncoder().encode("123456"),AuthorityUtils.createAuthorityList("common","xxl-job")));
            map.put("test_token2",newCustomUserDetails("root",newBCryptPasswordEncoder().encode("123456"),AuthorityUtils.createAuthorityList("common")));// 这里模拟从redis获取token对应的用户信息CustomUserDetails customUserDetail = map.get(token);if(customUserDetail !=null){UsernamePasswordAuthenticationToken authRequest =newUsernamePasswordAuthenticationToken(customUserDetail,null, customUserDetail.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authRequest);}}
        filterChain.doFilter(servletRequest, httpServletResponse);}/**
     * 从请求中获取token
     * @param servletRequest 请求对象
     * @return 获取到的token值 可以为null
     */privateStringgetToken(HttpServletRequest servletRequest){//先从请求头中获取String headerToken = servletRequest.getHeader("Authorization");if(StringUtils.hasText(headerToken)){return headerToken;}//再从请求参数里获取String paramToken = servletRequest.getParameter("accessToken");if(StringUtils.hasText(paramToken)){return paramToken;}returnnull;}}

主要完成的工作就是从请求头或者请求参数中获取token,与redis或其他存储介质中的进行比对,如果存在对应用户则正常访问,否则执行其他策略或者抛出异常。
这里内置了两个token,分别拥有不同权限。

编写密码加密及比对器

springsecurity中提供了一个

PasswordEncoder

接口,用于对密码进行加密和比对,我们也定义这样一个bean

packagecom.zjtx.tech.security.demo.config;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.DelegatingPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.crypto.password.Pbkdf2PasswordEncoder;importorg.springframework.security.crypto.scrypt.SCryptPasswordEncoder;importjava.util.HashMap;importjava.util.Map;@ConfigurationpublicclassPasswordEncoderConfig{/**
     * 获取密码编码方式
     */@Value("${password.encode.key:bcrypt}")privateString passwordEncodeKey;/**
     * 获取密码编码器
     * @return 密码编码器
     */@BeanpublicPasswordEncoderpasswordEncoder(){Map<String,PasswordEncoder> encoders =newHashMap<>();
        encoders.put("bcrypt",newBCryptPasswordEncoder());
        encoders.put("pbkdf2",Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        encoders.put("scrypt",newSCryptPasswordEncoder(4,8,1,32,16));returnnewDelegatingPasswordEncoder(passwordEncodeKey, encoders);}}

这里采用的实现类是

DelegatingPasswordEncoder

,一个好处是它可以兼容多种加密方式,区分的办法是根据加密后的字符串前缀,如bcrypt加密后的结果前缀就是{bcrypt},方便配置和扩展,不做过多阐述。

编写测试和登录用的controller

登录接口

packagecom.zjtx.tech.security.demo.controller;importcom.zjtx.tech.security.demo.common.Result;importcom.zjtx.tech.security.demo.provider.MobilecodeAuthenticationToken;importjakarta.annotation.Resource;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.Authentication;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.UUID;@RestController@RequestMapping("/login")publicclassLoginController{@ResourceprivateAuthenticationManager authenticationManager;/**
     * 用户名密码登录
     * @param username 用户名
     * @param password 密码
     * @return 返回登录结果
     */@GetMapping("/usernamePwd")publicResult<?>usernamePwd(String username,String password){UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =newUsernamePasswordAuthenticationToken(username, password);try{
            authenticationManager.authenticate(usernamePasswordAuthenticationToken);}catch(BadCredentialsException|UsernameNotFoundException e){thrownewServerException(e.getMessage());}String token =UUID.randomUUID().toString().replace("-","");returnResult.ok(token);}/**
     * 手机验证码登录
     * @param phone 手机号
     * @param mobileCode 验证码
     * @return 返回登录结果
     */@GetMapping("/mobileCode")publicResult<?>mobileCode(String phone,String mobileCode){MobilecodeAuthenticationToken mobilecodeAuthenticationToken =newMobilecodeAuthenticationToken(phone, mobileCode);Authentication authenticate;try{
            authenticate = authenticationManager.authenticate(mobilecodeAuthenticationToken);}catch(Exception e){
            e.printStackTrace();returnResult.error("验证码错误");}System.out.println(authenticate);String token =UUID.randomUUID().toString().replace("-","");returnResult.ok(token);}}

可以看到这个controller提供了用户名+密码登录和手机号+验证码登录两个接口。

测试用的接口:

packagecom.zjtx.tech.security.demo.controller;importorg.springframework.security.access.prepost.PreAuthorize;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("test")publicclassTestController{@GetMapping("demo")@PreAuthorize("hasAuthority('xxl-job')")publicStringdemo(){Authentication authentication =SecurityContextHolder.getContext().getAuthentication();System.out.println("authentication = "+ authentication);return"hello world";}}

这个controller定义了一个方法,这个方法需要用户拥有xxl-job的权限。

期望结果

结合我们之前定义的一些类,猜测期望结果应该是这样的:

  1. 使用用户名+密码登录时 如果是admin + 123456 可以正常登录 其他提示6000 登录失败
  2. 使用手机号+验证码登录时 如果是xxx + 6789 可以正常登录 其他提示6000 验证码错误
  3. 使用Authorization: test_token1访问demo接口时用户拥有common和xxl-job权限,可以正常访问demo接口
  4. 使用Authorization: test_token2访问demo接口时用户拥有common权限,访问demo接口时提示403 权限不足
  5. 使用其他token访问demo接口时提示401 用户未登录或token已过期

验证

启动项目,默认端口8080,使用postman模拟请求进行简单测试。

  1. 验证登录用户名+密码登录成功用户名+密码登录失败手机号+验证码登录成功手机号+验证码登录失败
  2. 验证接口访问访问demo接口成功访问demo接口403访问demo接口401

结论

经验证,结果

符合预期

总结

本文中我们完成了基于springboot3+springsecurity实现用户认证登录及鉴权访问的简单demo, 接下来我们会继续把获取及验证用户、生成token、校验token做个完善。

作为记录的同时也希望能帮助到需要的朋友。

创作不易,欢迎一键三连。


本文转载自: https://blog.csdn.net/u010361276/article/details/135640864
版权归原作者 泽济天下 所有, 如有侵权,请联系我们删除。

“【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)”的评论:

还没有评论