【工作记录】基于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 {
    @Serial
    private static final long serialVersionUID = 1L;

    // 状态码
    private int code;
    // 消息描述
    private String msg;
    // 数据内容
    private T data;

    public Result() {}

    public Result(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    // 成功响应构造器
    public static <T> Result<T> ok(T data) {
        return new Result<>(200, "success", data);
    }

    // 失败响应构造器
    public static <T> Result<T> fail(int code, String msg) {
        return new Result<>(code, msg, null);
    }

    // 错误响应构造器
    public static <T> Result<T> error(String errorMessage) {
        return new Result<>(500, errorMessage, null);
    }

    // getters and setters
    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

JSON转换工具类-JsonUtil.java

package com.zjtx.tech.security.demo.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.List;

public class JsonUtil {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 将Java对象转换为JSON字符串
     * @param obj 需要转换的Java对象
     * @return JSON格式的字符串
     */
    public static String toJson(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to convert object to JSON", e);
        }
    }

    /**
     * 将JSON字符串转换为指定类型的Java对象
     * @param jsonStr JSON格式的字符串
     * @param clazz 目标对象的Class类型
     * @param <T> 泛型类型
     * @return 转换后的Java对象实例
     */
    public static <T> T toObject(String jsonStr, Class<T> clazz) {
        try {
            return objectMapper.readValue(jsonStr, clazz);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to convert JSON string to object", e);
        }
    }

    /**
     * 将JSON字符串转换为指定类型的Java List对象
     * @param jsonStr JSON格式的字符串
     * @param elementType 列表中元素的Class类型
     * @param <T> 泛型类型
     * @return 转换后的Java List对象实例
     */
    public static <T> List<T> jsonToList(String jsonStr, Class<T> elementType) {
        try {
            JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, elementType);
            return objectMapper.readValue(jsonStr, javaType);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to convert JSON string to list", e);
        }
    }
}

自定义异常类-AuthorizationExceptionEx.java

package com.zjtx.tech.security.demo.exceptions;

import org.springframework.security.core.AuthenticationException;

public class AuthorizationExceptionEx extends AuthenticationException {

    public AuthorizationExceptionEx(String msg, Throwable cause) {
        super(msg, cause);
    }

    public AuthorizationExceptionEx(String msg) {
        super(msg);
    }
}

自定义异常类-ServerException.java

package com.zjtx.tech.security.demo.exceptions;

public class ServerException extends RuntimeException {

    public ServerException(String message) {
        super(message);
    }

    public ServerException(String message, Throwable cause) {
        super(message, cause);
    }

}

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

package com.zjtx.tech.security.demo.exceptions;

import com.zjtx.tech.security.demo.common.Result;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(AuthorizationExceptionEx.class)
    public Result<String> authorizationExceptionHandling(AuthorizationExceptionEx ex) {
        System.out.println("authorizationExceptionHandling = " + ex);
        return Result.fail(1000, ex.getMessage());
    }

    // handling specific exception
    @ExceptionHandler(ServerException.class)
    public Result<String> serverExceptionHandling(ServerException ex) {
        System.out.println("serverExceptionHandling = " + ex);
        return Result.fail(6000, ex.getMessage());
    }

    @ExceptionHandler(AccessDeniedException.class)
    public Result<String> accessDeniedExceptionHandling(AccessDeniedException ex) {
        System.out.println("accessDeniedExceptionHandling = " + ex);
        return Result.fail(403, "权限不足");
    }

    // handling global exception
    @ExceptionHandler(Exception.class)
    public Result<String> exceptionHandling(Exception ex) {
        System.out.println("exceptionHandling = " + ex);
        return Result.fail(500, "服务器内部异常,请稍后重试");
    }
}

开始

编写springsecurity配置类

MySecurityConfigurer.java

package com.zjtx.tech.security.demo.config;

import com.zjtx.tech.security.demo.provider.MobilecodeAuthenticationProvider;
import com.zjtx.tech.security.demo.provider.MyAuthenticationEntryPoint;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@EnableMethodSecurity
@EnableWebSecurity
@Configuration
public class MySecurityConfigurer {

    @Resource
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    @Resource
    private UserDetailsService customUserDetailsService;

    @Resource
    private PasswordEncoder passwordEncoder;

    @Resource
    private TokenAuthenticationFilter tokenAuthenticationFilter;

    @Bean
    public MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider() {
        MobilecodeAuthenticationProvider mobilecodeAuthenticationProvider = new MobilecodeAuthenticationProvider();
        mobilecodeAuthenticationProvider.setUserDetailsService(customUserDetailsService);
        return mobilecodeAuthenticationProvider;
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        daoAuthenticationProvider.setUserDetailsService(customUserDetailsService);
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        return daoAuthenticationProvider;
    }

    /**
     * 定义认证管理器AuthenticationManager
     * @return AuthenticationManager
     */
    @Bean
    public AuthenticationManager authenticationManager() {
        List<AuthenticationProvider> authenticationProviders = new ArrayList<>();
        authenticationProviders.add(mobilecodeAuthenticationProvider());
        authenticationProviders.add(daoAuthenticationProvider());
        return new ProviderManager(authenticationProviders);

    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
            throws Exception {
        http
                .authorizeHttpRequests((authorize) ->
                        authorize.requestMatchers(new AntPathRequestMatcher("/login/**")).permitAll()
                             .anyRequest().authenticated())
                .cors(Customizer.withDefaults())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(configure -> {
                    configure.authenticationEntryPoint(myAuthenticationEntryPoint);
                })
                .csrf(AbstractHttpConfigurer::disable);
        return http.build();
    }

}

这个类是springsecurity的统一配置类,不仅包含了AuthorizationProvider这个关键认证bean的定义,同时还定义了访问策略以及异常处理策略等信息。其中使用了springsecurity6中相对较新的语法,参考价值相对较高。

里面涉及到几个关键的bean,如下:

  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;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        MobilecodeAuthenticationToken mobilecodeAuthenticationToken = (MobilecodeAuthenticationToken) authentication;
        String phone = mobilecodeAuthenticationToken.getPhone();
        String mobileCode = mobilecodeAuthenticationToken.getMobileCode();
        System.out.println("登陆手机号:" + phone);
        System.out.println("手机验证码:" + mobileCode);

        // 模拟从redis中读取手机号对应的验证码及其用户名
        Map<String, String> dataFromRedis = new HashMap<>();
        dataFromRedis.put("code", "6789");
        dataFromRedis.put("username", "admin");

        // 判断验证码是否一致
        if (!mobileCode.equals(dataFromRedis.get("code"))) {
            throw new BadCredentialsException("验证码错误");
        }

        // 如果验证码一致,从数据库中读取该手机号对应的用户信息
        CustomUserDetails loadedUser = (CustomUserDetails) userDetailsService.loadUserByUsername(dataFromRedis.get("username"));
        if (loadedUser == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return new MobilecodeAuthenticationToken(loadedUser, null, loadedUser.getAuthorities());

    }

    @Override
    public boolean supports(Class<?> aClass) {
        return MobilecodeAuthenticationToken.class.isAssignableFrom(aClass);
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

说明如下:

  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 {

    @Resource
    private PasswordEncoder passwordEncoder;

    private static final Collection<GrantedAuthority> authorities = new ArrayList<>();

    static {
        GrantedAuthority defaultRole = new SimpleGrantedAuthority("common");
        GrantedAuthority xxlJobRole = new SimpleGrantedAuthority("xxl-job");
        authorities.add(defaultRole);
        authorities.add(xxlJobRole);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws AuthenticationException {
        CustomUserDetails userDetails;
        // 这里模拟从数据库中获取用户信息
        if (username.equals("admin")) {
            //这里的admin用户拥有common和xxl-job两个权限
            userDetails = new CustomUserDetails("admin", passwordEncoder.encode("123456"), authorities);
            userDetails.setAge(25);
            userDetails.setSex(1);
            userDetails.setAddress("xxxx小区");
            return userDetails;
        } else {
            throw new UsernameNotFoundException("用户不存在");
        }
    }
}

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

还涉及到MobilecodeAuthenticationToken.java这个类,如下:

package com.zjtx.tech.security.demo.provider;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * 手机验证码认证信息,在UsernamePasswordAuthenticationToken的基础上添加属性 手机号、验证码
 */
public class MobilecodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 530L;
    private Object principal;
    private Object credentials;
    private String phone;
    private String mobileCode;


    public MobilecodeAuthenticationToken(String phone, String mobileCode) {
        super(null);
        this.phone = phone;
        this.mobileCode = mobileCode;
        this.setAuthenticated(false);
    }

    public MobilecodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public String getPhone() {
        return phone;
    }

    public String getMobileCode() {
        return mobileCode;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        } else {
            super.setAuthenticated(false);
        }
    }

    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}

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

package com.zjtx.tech.security.demo.provider;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;
import java.util.List;

public class CustomUserDetails extends User {

    private int age;

    private int sex;

    private String address;

    private String phone;

    private List<String> roles;

    public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getSex() {
        return sex;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }
}

继承了org.springframework.security.core.userdetails.User这个类同时添加了一些自定义属性,可自行扩展。

编写认证异常处理类

上面在安全配置类中用到了这个异常处理类,主要处理认证异常和访问被拒绝。

MyAuthenticationEntryPoint.java

package com.zjtx.tech.security.demo.provider;

import com.zjtx.tech.security.demo.common.Result;
import com.zjtx.tech.security.demo.util.JsonUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint, AccessDeniedHandler {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        Result<String> result = Result.fail(401, "用户未登录或已过期");
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JsonUtil.toJson(result));
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result<String> result = Result.fail(403, "权限不足");
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JsonUtil.toJson(result));
    }
}

比较简单,实现了两个接口,返回不同的json数据。JsonUtil比较简单,就不在此列出了。

编写认证过滤器类

过滤器在认证中扮演者非常重要的角色,我们也定义了一个用于token校验的filter,如下:

package com.zjtx.tech.security.demo.config;

import com.zjtx.tech.security.demo.provider.CustomUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
@WebFilter
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest servletRequest, @NonNull HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException, ServletException {
        String token = getToken(servletRequest);
        // 如果没有token,跳过该过滤器
        if (StringUtils.hasText(token)) {
            // 模拟redis中的数据
            Map<String, CustomUserDetails> map = new HashMap<>();
            //这里放入了两个示例token 仅供测试
            map.put("test_token1", new CustomUserDetails("admin", new BCryptPasswordEncoder().encode("123456"), AuthorityUtils.createAuthorityList("common", "xxl-job")));
            map.put("test_token2", new CustomUserDetails("root", new BCryptPasswordEncoder().encode("123456"), AuthorityUtils.createAuthorityList("common")));

            // 这里模拟从redis获取token对应的用户信息
            CustomUserDetails customUserDetail = map.get(token);
            if (customUserDetail != null) {
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(customUserDetail, null, customUserDetail.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authRequest);
            }
        }
        filterChain.doFilter(servletRequest, httpServletResponse);

    }

    /**
     * 从请求中获取token
     * @param servletRequest 请求对象
     * @return 获取到的token值 可以为null
     */
    private String getToken(HttpServletRequest servletRequest) {
        //先从请求头中获取
        String headerToken = servletRequest.getHeader("Authorization");
        if(StringUtils.hasText(headerToken)) {
            return headerToken;
        }
        //再从请求参数里获取
        String paramToken = servletRequest.getParameter("accessToken");
        if(StringUtils.hasText(paramToken)) {
            return paramToken;
        }
        return null;

    }
}

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

编写密码加密及比对器

springsecurity中提供了一个PasswordEncoder接口,用于对密码进行加密和比对,我们也定义这样一个bean

package com.zjtx.tech.security.demo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class PasswordEncoderConfig {

    /**
     * 获取密码编码方式
     */
    @Value("${password.encode.key:bcrypt}")
    private String passwordEncodeKey;

    /**
     * 获取密码编码器
     * @return 密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
        encoders.put("scrypt", new SCryptPasswordEncoder(4,8, 1,32, 16));
        return new DelegatingPasswordEncoder(passwordEncodeKey, encoders);
    }

}

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

编写测试和登录用的controller

登录接口

package com.zjtx.tech.security.demo.controller;

import com.zjtx.tech.security.demo.common.Result;
import com.zjtx.tech.security.demo.provider.MobilecodeAuthenticationToken;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping("/login")
public class LoginController {

    @Resource
    private AuthenticationManager authenticationManager;

    /**
     * 用户名密码登录
     * @param username 用户名
     * @param password 密码
     * @return 返回登录结果
     */
    @GetMapping("/usernamePwd")
    public Result<?> usernamePwd(String username, String password) {
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        try {
            authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        } catch (BadCredentialsException | UsernameNotFoundException e) {
            throw new ServerException(e.getMessage());
        }
        String token = UUID.randomUUID().toString().replace("-", "");
        return Result.ok(token);
    }

    /**
     * 手机验证码登录
     * @param phone 手机号
     * @param mobileCode 验证码
     * @return 返回登录结果
     */
    @GetMapping("/mobileCode")
    public Result<?> mobileCode(String phone, String mobileCode) {
        MobilecodeAuthenticationToken mobilecodeAuthenticationToken = new MobilecodeAuthenticationToken(phone, mobileCode);
        Authentication authenticate;
        try {
            authenticate = authenticationManager.authenticate(mobilecodeAuthenticationToken);
        } catch (Exception e) {
            e.printStackTrace();
            return Result.error("验证码错误");
        }
        System.out.println(authenticate);
        String token = UUID.randomUUID().toString().replace("-", "");
        return Result.ok(token);
    }
}

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

测试用的接口:

package com.zjtx.tech.security.demo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("test")
public class TestController {

    @GetMapping("demo")
    @PreAuthorize("hasAuthority('xxl-job')")
    public String demo(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        System.out.println("authentication = " + authentication);
        return "hello world";
    }

}

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

期望结果

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

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

NVMe-oF RDMA vs. TCP延时测试对比:端到端SPDK的意义

前不久看到一篇《NVIDIA BlueField 再创 DPU 性能世界纪录》的新闻&#xff0c;该测试环境是2台服务器&#xff0c;每台各安装2块NVIDIA Bluefield-2 DPU&#xff0c;形成4条100GbE以太网直连&#xff0c;两端分别跑NVMe-oF Target&#xff08;存储目标&#xff09;和Initiator…

Spring IoC 和 DI

文章目录 1. 什么是 Spring2. 什么是 IoC3. 什么是 DI4. IoC & DI 使用5. 获取 Bean 的方式5.1 根据类型获取 bean5.2 根据名称获取 bean5.3 获取bean对象的其他方式5.4 五大注解的关联 6. 方法注解7. 扫描路径8. Bean 的名称9. DI 详解9.1 属性注入9.2 构造方法注入9.3 Se…

残差网络 ResNet

目录 1.1 ResNet 2.代码实现 1.1 ResNet 如上图函数的大小代表函数的复杂程度&#xff0c;星星代表最优解&#xff0c;可见加了更多层之后的预测比小模型的预测离真实最优解更远了&#xff0c; ResNet做的事情就是使得模型加深一定会使效果变好而不是变差。 2.代码实现 impo…

【OpenAI】自定义GPTs应用(GPT助手应用)及外部API接口请求

11月10日&#xff0c;OpenAI正式宣布向所有ChatGPT Plus用户开放GPTs功能 简而言之&#xff1a;GPT应用市场(简称GPTs, 全称GPT Store) Ps&#xff1a; 上图为首次进入时的页面&#xff0c;第一部分是自己创建的GPTs应用&#xff0c;下面是公开可以使用的GPTs应用 一、创建GPTs…

Spring Cloud 微服务中 gateway 网关如何设置健康检测端点

主要是为了让 k8s 识别到网关项目已经就绪&#xff0c;但是又不想在里面通过 Controller 实现。因为在 Controller 中这样做并不是最佳实践&#xff0c;因为 Gateway 的设计初衷是专注于路由和过滤&#xff0c;而不是业务逻辑的处理。 在 Gateway 中配置健康检查端点可以通过以…

单向不带头链表的使用

单向不带头链表的使用 链表的创建&#xff1a; typedef struct LNode {SLDataType data;struct LNode* next; }LNode,*LinkList; 按位查找 LNode* GetElem(LinkList L, int i) {int j 1;LNode* p L->next;if (i < 0)return NULL;if (i 0)return L;while (p &&…

4种方法用Python批量实现多Excel多Sheet合并

目录 方法一&#xff1a;使用pandas库 方法二&#xff1a;使用openpyxl库 方法三&#xff1a;使用xlrd和xlwt库 方法四&#xff1a;使用os和glob库 在数据处理中&#xff0c;经常需要将多个Excel文件中的多个工作表进行合并。以下介绍了4种方法&#xff0c;使用Python批量实…

消费增值模式:引领消费者与平台共创双赢的新篇章

在数字化时代&#xff0c;消费模式正在发生深刻变革。消费者不再满足于单纯的购物行为&#xff0c;而是寻求更加个性化和有价值的消费体验。而平台也面临着如何吸引和留住消费者的挑战。消费增值模式作为一种新型的商业模式&#xff0c;正逐渐成为解决这一问题的关键。 消费增…

Java多线程并发篇----第十八篇

系列文章目录 文章目录 系列文章目录前言一、寄存器二、程序计数器三、PCB-“切换桢”四、上下文切换的活动五、引起线程上下文切换的原因前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了…

3D Guassians Splatting相关解读

从已有的点云模型出发&#xff0c;以每个点为中心&#xff0c;建立可学习的高斯表达&#xff0c;用Splatting即抛雪球的方法进行渲染&#xff0c;实现高分辨率的实时渲染。 1、主要思想 1.引入了一种各向异性&#xff08;anisotropic&#xff09;的3D高斯分布作为高质量、非结…

查看centos的CPU、内存、磁盘空间等配置信息

目录 查看CPU/proc/cpuinfo中的信息 查看内存/proc/meminfo中的信息 查看磁盘空间df 命令du命令使用fdisk命令 查看CPU /proc/cpuinfo中的信息 前置&#xff1a; [ltkjltkj front]$ cat /proc/cpuinfo| grep "physical id" physical id : 0 physical id : 0 physi…

智慧校园大数据平台架构

平台架构 基础硬件层 基础硬件层是由一组低廉的PC或服务器组合构建而成。基础硬件层主要承载着数据的存储、运算、容错、调度和通信等任务,对基础应用层下达的指令进行执行和反馈。 数据集成 大数据特征表现在实时、交互、海量等方面,并且以半结构化、非结构化数据为主,价…

机器学习系统或者SysMLDL笔记

在使用过TVM、TensorRT等优秀的机器学习编译优化系统以及Pytorch、Keras等深度学习框架后&#xff0c;总觉得有必要从理论上对这些系统进行一些分析&#xff0c;虽然说在实践中学习是最快最直接的(指哪儿打哪儿、不会哪儿查哪儿)&#xff0c;但恶补一些关于系统设计的一些知识还…

搜索经典题——填充 9*9矩阵

题目&#xff1a;给定一个九行九列矩阵&#xff0c;填充矩阵元素&#xff0c;要求&#xff1a; 1、每一行每一列&#xff0c;每个小九宫格&#xff08;图片画粗的地方就是&#xff09;不能包含相同元素 2、每一行&#xff0c;每一列&#xff0c;每个小九宫格均会完整出现1-9的数…

Python进程池multiprocessing.Pool

环境&#xff1a; 鲲鹏920:192核心 内存&#xff1a;756G python&#xff1a;3.9 python单进程的耗时 在做单纯的cpu计算的场景&#xff0c;使用单进程核多进程的耗时做如下测试&#xff1a; 单进程情况下cpu的占用了如下&#xff0c;占用一半的核心数&#xff1a; 每一步…

第二百六十九回

文章目录 概念介绍设置方法示例代码内容总结 我们在上一章回中介绍了Card Widget相关的内容&#xff0c;本章回中将介绍国际化设置.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在这里说的国际化设置是指在App设置相关操作&#xff0c;这样可以让不同国家的…

SAP PI之Rest adapter

一&#xff0c;简介 REST风格接口是以http为传输协议&#xff0c;以xml或json或text为有效负载。下图展示了REST到XI再返回的一个过程&#xff0c;一个REST接口包含的信息有&#xff1a;服务URL、URL中带的参数、http方法(post/get/put等)、http头部、body部分的有效载荷。而X…

2023年全球软件质量效能大会(QECon北京站):核心内容与学习收获(附大会核心PPT下载)

此次大会的主题为“智能时代的质量新篇章”。来自全球的软件质量与效能专家、企业领袖、技术研发人员等齐聚一堂&#xff0c;共同探讨软件质量与效能的新理念、新技术、新实践。 一、大会的核心内容 1、智能时代软件质量的新挑战与机遇 随着人工智能、大数据等技术的快速发展…

react、Vue打包直接运行index.html不空白方法

react vue 在根目录下创建 vue.config.js 文件&#xff0c;写入 module.exports {publicPath: ./, }

【SpringCloud】这一次终于使用MQ解决了Eureka服务下线延迟感知问题

前言 其实&#xff0c;“通过Redis手动更新Ribbon缓存来解决Eureka微服务架构中服务下线感知的问题”是一种解&#xff0c;但不是最优解 1.痛点 上一篇文章的标题是&#xff1a; 通过Redis手动更新Ribbon缓存来解决Eureka微服务架构中服务下线感知的问题 当时在文章的末尾就…