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

Spring 核心之 IOC 容器学习二

基于 Annotation 的 IOC 初始化 Annotation 的前世今生 从 Spring2.0 以后的版本中&#xff0c;Spring 也引入了基于注解(Annotation)方式的配置&#xff0c;注解(Annotation)是 JDK1.5 中引入的一个新特性&#xff0c;用于简化 Bean 的配置&#xff0c;可以取代 XML 配置文件…

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

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

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

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

WPF中Image控件Source的多种指定方式

XAML中 1、直接绝对路径直接给Source 2、将图片放到项目里面&#xff0c;设置图片为资源&#xff1b;Source写法为&#xff1a; &#xff08;1&#xff09;Source"pack://application:,,,/label里面的Content;component/folder/test.png" &#xff08;2&…

HBase学习六:LSM树算法

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

鸿蒙应用开发横空出世:是否应该换赛道

鸿蒙应用开发横空出世:互联网寒冬的希望? 大家好,我是demo.最近相信大家最近也收到了这样的消息,网上大肆宣传明年未来的趋势是鸿蒙应用开发,这里呢我也确实被这些信息所覆盖.最近几年确实互联网行业不是很景气,许多公司大量裁员,很多人都因此丢了工作.大龄程序员一直是互联网…

5408. 保险箱

5408. 保险箱 - AcWing题库 #include <iostream> #include <string> #include <algorithm> #include <cstring> using namespace std;const int N 1e5 10; string x, y; int f[N][3], n; int main() {cin >> n >> x >> y;memset(…

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 和…

drive souls to hell

​ a man forgot to put his phone on silent (/ˈsaɪlənt/), and it rang so loud in the church during preaching, the pastor scolded him,the worshippers admonished him after the sermon for interrupting ,his wife kept to lecturing on his carelessness all the …

用Python判断节假日,以及节假日的SQL数据文件

为什么要判断节假日&#xff1f; 在我们的日常生活中&#xff0c;节假日是一个重要的组成部分。无论是个人计划还是商业活动&#xff0c;了解特定日期是否为节假日都是非常有用的。在Python中&#xff0c;我们可以使用一些内置的日期和时间模块来判断一个日期是否是法定节假日…

再见了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 以及…