【工作记录】基于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"?>
<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>

相对来说比较简单:

  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

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,如下:

  1. MyAuthenticationEntryPoint 自定义的异常处理类,用于处理认证异常及访问被拒绝异常
  2. UserDetailsService springsecurity提供的获取用户信息的一个接口,需要使用者自行完善
  3. PasswordEncoder 密码加密方法类,由使用者自行扩展
  4. TokenAuthenticationFilter 自定义的请求token校验过滤器
  5. 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;}
}

说明如下:

  1. 上面类中比较关键的就是authenticatesupport方法,如果看过一点源码的话可以知道这里会存在多个Provider,通过support方法来确定使用哪个Provider的实现类。

  2. authenticate就是具体的认证逻辑,如判断验证码是否正确,根据手机号查找用户信息等。

  3. 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的权限。

期望结果

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

  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做个完善。

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/632447.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux miniGUI移植分析

框架介绍 常用GUI程序对比 https://www.cnblogs.com/zyly/p/17378659.html MiniGUI分为底层的GAL&#xff08;图形抽象层&#xff09;和IAL&#xff08;输入抽象层&#xff09;&#xff0c;向上为基于标准POSIX接口中pthread库的Mini-Thread架构和基于Server/Client的Mini-L…

JSONObject - 用最通俗的讲解,教你玩转 JSON 数据的解析和修改

目录 一、JSONObject 1.1、为什么要使用他&#xff1f; 1.2、应用 1.2.1、依赖 1.2.2、JSON 数据示例 1.2.3、JSON 数据的构建 1.2.4、JSON 数据的解析 一、JSONObject 1.1、为什么要使用他&#xff1f; 在还没有接触过这个东西的时候&#xff0c;一直是通过 ObjectMap…

深度学习记录--归—化输入特征

归化 归化输入(normalizing inputs),对特征值进行一定的处理&#xff0c;可以加速神经网络训练速度 步骤 零均值化 通过x值更新让均值稳定在零附近&#xff0c;即为零均值化 归化方差 适当减小变量方差 解释 归化可以让原本狭长的数据图像变得规整&#xff0c;梯度下降的…

Electron中苹果支付 Apple Pay inAppPurchase 内购支付

正在开发中&#xff0c;开发好了&#xff0c;写一个完整详细的过程&#xff0c;保证无脑集成即可 一、先创建一个App 一般情况下&#xff0c;在你看这篇文章的时候&#xff0c;说明你已经开发的app差不多了。 但是要上架app到Mac App Store&#xff0c;则要在appstoreconnect…

HBase学习六:LSM树算法

1、简介 HBase是基于LSM树架构实现的,天生适合写多读少的应用场景。 LSM树本质上和B+树一样,是一种磁盘数据的索引结构。但和B+树不同的是,LSM树的索引对写入请求更友好。因为无论是何种写入请求,LSM树都会将写入操作处理为一次顺序写,而HDFS擅长的正是顺序写(且HDFS不…

Linux第31步_了解STM32MP157的TF-A

了解STM32MP157的TF-A&#xff0c;为后期移植服务。 一、指令集 ARMV8提供了两种指令集:AAarch64和AArch32&#xff0c;根据字面意思就是64位和32位。 ARMV7提供的指令集是AArch32。 二、TF-A 指令集是AArch64的芯片&#xff0c;TF-A有&#xff1a;bl1、bl2、bl31、bl32 和…

再见了RDM,Redis官方GUI才是最好的!

1 简介 直观高效的 Redis GUI 管理工具&#xff0c;它可以对 Redis 的内存、连接数、命中率以及正常运行时间进行监控&#xff0c;并且可以在界面上使用 CLI 和连接的 Redis 进行交互&#xff08;RedisInsight 内置对 Redis 模块支持&#xff09;&#xff0c;官方下载地址。 使…

Power Designer 连接 PostgreSQL 逆向工程生成pd表结构操作步骤以及过程中出现的问题解决

一、使用PowerDesigner16.5 链接pg数据库 1.1、启动PD.选择Create Model…。 1.2、选择Model types / Physical Data Model Physical Diagram&#xff1a;选择pgsql直接【ok】 1.3、选择connect 在工具栏选择Database-Connect… 快捷键&#xff1a;ctrlshiftN.如下图&#xff…

查询数据库表字段具有某些特征的表

目录 引言举例总结 引言 当我们把一个项目做完以后&#xff0c;客户要求我们把系统中所有的电话&#xff0c;证件号等进行加密处理时&#xff0c;我们难道要一个表一表去查看那些字段是电话和证件号码吗&#xff1f; 这种办法有点费劲&#xff0c;下面我们来探索如何找到想要的…

CVE-2024-0195-SpiderFlow爬虫平台远程命令执行漏洞分析

项目下载地址 spider-flow: 新一代爬虫平台&#xff0c;以图形化方式定义爬虫流程&#xff0c;不写代码即可完成爬虫。https://gitee.com/ssssssss-team/spider-flow 在平台spiderflow的页面中有一个自定义函数&#xff0c;看到函数应是非常的敏感了。 可以做一些猜想与尝试&…

「优选算法刷题」:盛最多水的容器

一、题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不能倾斜容器…

RT Thread Stdio生成STM32L431RCT6无法启动问题

一、问题现象 使用RT thread Stdio生成STM32L431RCT6工程后&#xff0c;编译下载完成后系统无法启动&#xff0c;无法仿真debug&#xff1b; 二、问题原因 如果当前使用的芯片支持包版本为0.2.3&#xff0c;可能是这个版本问题&#xff0c;目前测试0.2.3存在问题&#xff0c…

【白话机器学习的数学】读书笔记(4)评估(评估已建立的模型)

四、评估(评估已建立的模型) 目录 四、评估(评估已建立的模型)1.评估什么2.交叉验证1 回归问题的验证2 分类问题的验证3 精确率和召回率1.精确率Precision2.召回率Recall 4 F值5 K折交叉验证 3.正则化1 正则化的方法2 正则化的效果3 分类的正则化4 包含正则化项的表达式的微分1…

docker安装运行CloudBeaver并设置默认语言为中文

1、CloudBeaver CloudBeaver 是一个开源的 Web 数据库管理工具&#xff0c;它提供了一个基于浏览器的用户界面&#xff0c;允许用户管理和操作各种类型的数据库。CloudBeaver 支持多种数据库系统&#xff0c;包括但不限于 PostgreSQL、MySQL、SQLite、Oracle、SQL Server 以及…

安全帽识别:智能监控新趋势

在现代工业安全领域&#xff0c;安全帽识别技术已成为一项关键的创新。这项技术通过智能监控系统确保工作人员在危险环境中佩戴安全帽&#xff0c;显著提升了工作场所的安全标准。本文将探讨这一技术的工作原理、应用前景及其在现代工业中的重要性。 安全帽识别的工作机制 安全…

YOLOv5全网独家首发:DCNv4更快收敛、更高速度、更高性能,效果秒杀DCNv3、DCNv2等 ,助力检测实现暴力涨点

💡💡💡本文独家改进:DCNv4更快收敛、更高速度、更高性能,完美和YOLOv5结合,助力涨点 DCNv4优势:(1) 去除空间聚合中的softmax归一化,以增强其动态性和表达能力;(2) 优化存储器访问以最小化冗余操作以加速。这些改进显著加快了收敛速度,并大幅提高了处理速度,DCN…

超级详细的linux centos NFS共享服务器搭建

目录 背景说明: 1.服务端操作 1.1创建目录 1.2创建组 1.3创建用户 1.4目录授权给www:www 1.5安装nfs服务端 1.6配置权限 1.7启动服务 2.客户端操作 2.1安装软件 2.2创建目录 2.3挂载 2.4测试 2.4.1读写删除测试 2.4.1只读测试 背景说明: 看了一个帖子NFS教程,…

设备树下Led驱动实验-Led驱动加载测试

一. 简介 本文对前面两篇文章实现的 Led驱动程序进行测试。 通过应用程序调用驱动程序&#xff0c;从而进行 Led灯的打开与关闭。 二. Led驱动加载测试 1. 准备测试程序 将 前面实验中 3_newchrled工程中应用程序拷贝到 5_dtsled工程目录下。 进入 5_dtsled工程目录下进…

uncaught referenceError:cannot access ‘xxxStore‘ before initialization

目录 一、问题 二、解决方法 三、总结 tiips:如嫌繁琐&#xff0c;直接移步总结即可&#xff01; 一、问题 1.写代码的时候引入store&#xff0c;居然报错&#xff1a;store没有初始化就被使用了&#xff0c;连页面都打不开了。 1)错误如下&#xff0c;详细错误如下图1-1所…

瑞_Java开发手册_(六)工程结构

文章目录 工程结构的意义(一) 应用分层(二) 二方库依赖(三) 服务器 &#x1f64a;前言&#xff1a;本文章为瑞_系列专栏之《Java开发手册》的工程结构篇&#xff0c;主要介绍应用分层、二方库依赖、服务器。由于博主是从阿里的《Java开发手册》学习到Java的编程规约&#xff0c…