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

前言

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

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

目标

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

准备工作

1. 新建项目

pom.xml (供参考)


<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.0modelVersion>

    <groupId>com.zjtx.tech.securitygroupId>
    <artifactId>security_demoartifactId>
    <version>1.0-SNAPSHOTversion>

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>3.1.2version>
    parent>

    <properties>
        <maven.compiler.source>20maven.compiler.source>
        <maven.compiler.target>20maven.compiler.target>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        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  泛型类型
     * @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  泛型类型
     * @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 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 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. 验证登录
    【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_第1张图片
    【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_第2张图片
    【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_第3张图片
    【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_第4张图片
  2. 验证接口访问
    【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_第5张图片
    【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_第6张图片
    【工作记录】基于springboot3+springsecurity6实现多种登录方式(一)_第7张图片

结论

经验证,结果符合预期

总结

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

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

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

你可能感兴趣的:(工作记录,java,springboot3,springsecurity,认证登录)