一 、 Springboot2.7 + springsecurityoauth2 + jwt (授权服务)

86 阅读10分钟

创建父工程 spring-security

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.elew</groupId>
    <artifactId>spring-security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>spring-security</name>
    <description>spring-security</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.7.6</spring-boot.version>
        <spring-cloud.version>2021.0.5</spring-cloud.version>
    </properties>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.5.2</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <modules>
        <module>spring-auth-server</module>
    </modules>


    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

创建模块工程 spring-auth-server

支持授权码模式、简单模式、客户端模式、自定义短信模式、刷新token 密码模式

  1. pom.xml 引入依赖
<dependencies>
    <!-- Spring Boot 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Security 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- OAuth2 依赖 -->
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    </dependency>

    <!-- JWT 依赖 -->
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-jwt</artifactId>
        <version>1.1.1.RELEASE</version>
    </dependency>

    <!-- MyBatis-Plus 依赖 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>

    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok 依赖 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

</dependencies>
项目配置文件
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://192.168.3.101:3306/spring_security?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver


mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.springauthserver.entity
  configuration:
    map-underscore-to-camel-case: true

jwt:
  signingKey: elewkey
1.服务授权配置类 AuthServerConfig.java
package com.elew.springauthserver.conf;

import com.elew.springauthserver.service.OAuthClientDetailsService;
import com.elew.springauthserver.sms.SmsCodeAuthorizationGrantType;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.*;

import java.util.*;


@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {


    // 注入 AuthenticationManager,用于验证用户的身份信息
    private final AuthenticationManager authenticationManager;

    // 注入 UserDetailsService,用于加载用户的详细信息
    private final UserDetailsService userDetailsService;

    // 注入 OAuthClientDetailsService,用于加载客户端的详细信息
    private final OAuthClientDetailsService oAuthClientDetailsService;

    private final AuthorizationServerTokenServices tokenService;

    private final TokenStore tokenStore;

    public AuthServerConfig(AuthenticationManager authenticationManager, UserDetailsService userDetailsService, OAuthClientDetailsService oAuthClientDetailsService, AuthorizationServerTokenServices tokenService, TokenStore tokenStore) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.oAuthClientDetailsService = oAuthClientDetailsService;
        this.tokenService = tokenService;
        this.tokenStore = tokenStore;
    }


    // 配置授权服务器的安全规则,如令牌端点的访问权限
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
                .tokenKeyAccess("permitAll()") // 允许所有人访问令牌密钥端点
                .checkTokenAccess("permitAll()") // 只有认证用户才能访问令牌校验端点
                .allowFormAuthenticationForClients(); // 允许客户端使用表单进行认证
    }


    /**
     * 配置自定义的 TokenGranter,用于支持短信验证码登录
     *
     * @param endpoints
     * @throws Exception
     */
    private TokenGranter tokenGranter(final AuthorizationServerEndpointsConfigurer endpoints) {
        List<TokenGranter> granters = new ArrayList<>(Collections.singletonList(endpoints.getTokenGranter()));
        granters.add(new SmsCodeAuthorizationGrantType(
                tokenService,
                endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(),
                userDetailsService
        ));
        return new CompositeTokenGranter(granters);
    }

    // 配置客户端详情服务,包括客户端 ID、密钥、授权类型、作用域等。
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientId -> oAuthClientDetailsService.loadClientByClientId(clientId));
    }


    // 配置授权服务器的端点,如授权端点、令牌端点,以及令牌的存储和管理
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) // 密码模式需要
                .userDetailsService(userDetailsService) 
                .tokenStore(tokenStore)
                .tokenServices(tokenService)
                .tokenGranter(tokenGranter(endpoints))
        ;
    }


}
2. Token配置类 TokenConfig.java
package com.elew.springauthserver.conf;

import com.elew.springauthserver.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.*;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.util.*;

@Configuration
public class TokenConfig {

    @Value("${jwt.signingKey}")
    private String signingKey;

    /**
     * 配置 TokenServices
     * // 标记:@Primary 默认自动注入了 defaultAuthorizationServerTokenServices:由 org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration 类中的 defaultAuthorizationServerTokenServices 方法定义
     * @return
     */
    @Bean
    @Primary
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        // 设置 TokenStore
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setTokenEnhancer(tokenEnhancerChain());
        // 可根据需求设置其他属性
        return tokenServices;
    }


    /**
     * 配置 JWT 令牌存储
     * @return TokenStore 实例
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 配置 JWT 访问令牌转换器
     * @return JwtAccessTokenConverter 实例
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 设置签名密钥,实际应用中应使用更安全的方式
        converter.setSigningKey(signingKey);
        return converter;
    }

    /**
     * 配置令牌增强链
     * @return TokenEnhancerChain 实例
     */
    @Bean
    public TokenEnhancerChain tokenEnhancerChain() {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
        return tokenEnhancerChain;
    }

    /**
     * 自定义令牌增强器,可添加额外的声明
     * @return TokenEnhancer 实例
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {

            User user = (User) authentication.getPrincipal();
            // 获取用户权限信息
            List<String> authorities = new ArrayList<>();
            authentication.getAuthorities().forEach(grantedAuthority -> {
                authorities.add(grantedAuthority.getAuthority());
            });
            // 添加用户权限信息到 JWT 载荷
            Map<String, Object> additionalInfo = new HashMap<>();
            additionalInfo.put("authorities", authorities);
            additionalInfo.put("user_id", user.getId());
            additionalInfo.put("username", user.getUsername());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);

            return accessToken;
        };
    }
}
3. 请求安全规则配置 SecurityConfig.java
package com.elew.springauthserver.conf;


import com.elew.springauthserver.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests(auth -> auth.antMatchers("/oauth/**").permitAll() // 允许访问登录页面
                .anyRequest().authenticated() // 其他请求需要认证
        ).formLogin().and().httpBasic();
    }

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance();
    }
}
4. 自定义授权类型 SmsCodeAuthorizationGrantType.java

自定义授权 继承 AbstractTokenGranter 实现方法 getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest)

package com.elew.springauthserver.sms;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.token.TokenService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

import java.util.Map;

public class SmsCodeAuthorizationGrantType extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "sms_code";
    private final UserDetailsService userDetailsService;

    public SmsCodeAuthorizationGrantType(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, UserDetailsService userDetailsService) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = tokenRequest.getRequestParameters();
        String mobile = parameters.get("mobile");
        String code = parameters.get("code");
        // 验证短信验证码
        if (!validateSmsCode(mobile, code)) {
            throw new InvalidGrantException("Invalid sms code");
        }
        // 加载用户信息
        UserDetails user = userDetailsService.loadUserByUsername(mobile);

        // 创建认证信息
        UsernamePasswordAuthenticationToken userAuth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        userAuth.setDetails(parameters);
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }

    private boolean validateSmsCode(String mobile, String code) {
        // 实现短信验证码验证逻辑
        // 这里简单返回 true,实际应用中需要验证验证码是否正确
        return true;
    }
}
5. 实体类
package com.elew.springauthserver.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("oauth_client_details")
public class OAuthClientDetails {
    private String clientId;
    private String resourceIds;
    private String clientSecret;
    private String scope;
    private String authorizedGrantTypes;
    private String webServerRedirectUri;
    private String authorities;
    private Integer accessTokenValidity;
    private Integer refreshTokenValidity;
    private String additionalInformation;
    private String autoapprove;
}
package com.elew.springauthserver.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;

@Data
@TableName("permissions")
public class Permission {

    private Long id;
    private String permissionName;
    private String permissionCode;
    private String description;
    private String resourceType;
    private String resourceUrl;
    private Integer status;
    private Date createdAt;
    private Date updatedAt;
}
package com.elew.springauthserver.entity;


import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.*;

@Data
@TableName("users")
public class User implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String phone;
    private String realName;
    private String avatar;
    private Integer status;
    private Date createdAt;
    private Date updatedAt;

    @TableField(exist = false)
    private Set<Role> roles;

    // 标记为数据库不存在的字段
    @TableField(exist = false)
    private boolean accountNonExpired = true;
    @TableField(exist = false)
    private boolean accountNonLocked = true;
    @TableField(exist = false)
    private boolean credentialsNonExpired = true;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        if (roles != null) {
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode()));
                if (role.getPermissions() != null) {
                    for (Permission permission : role.getPermissions()) {
                        authorities.add(new SimpleGrantedAuthority(permission.getPermissionCode()));
                    }
                }
            }
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return status == 1;
    }
}
package com.elew.springauthserver.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.util.Date;
import java.util.Set;


@Data
@TableName("roles")
public class Role {

    private Long id;
    private String roleName;
    private String roleCode;
    private String description;
    private Integer status;
    private Date createdAt;
    private Date updatedAt;

    @TableField(exist = false)
    private Set<Permission> permissions;
}
6. mapper
package com.elew.springauthserver.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.elew.springauthserver.entity.OAuthClientDetails;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OAuthClientDetailsMapper extends BaseMapper<OAuthClientDetails> {
}
package com.elew.springauthserver.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.elew.springauthserver.entity.Permission;

public interface PermissionMapper extends BaseMapper<Permission> {
}
package com.elew.springauthserver.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.elew.springauthserver.entity.Permission;
import com.elew.springauthserver.entity.Role;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.Set;

@Mapper
public interface RoleMapper extends BaseMapper<Role> {

    Set<Permission> selectPermissionsByRoleId(@Param("roleId") Long roleId);
}
package com.elew.springauthserver.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.elew.springauthserver.entity.Role;
import com.elew.springauthserver.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.Set;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    Set<Role> selectRolesByUserId(@Param("userId") Long userId);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.elew.springauthserver.mapper.RoleMapper">
    <select id="selectPermissionsByRoleId" resultType="com.elew.springauthserver.entity.Permission">
        SELECT p.* FROM permissions p
                            JOIN role_permissions rp ON p.id = rp.permission_id
        WHERE rp.role_id = #{roleId}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.elew.springauthserver.mapper.UserMapper">

    <select id="selectRolesByUserId" resultType="com.elew.springauthserver.entity.Role">
        SELECT r.* FROM roles r JOIN user_roles ur ON r.id = ur.role_id
        WHERE ur.user_id = #{userId}
    </select>
</mapper>
7. Service
package com.elew.springauthserver.service;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.elew.springauthserver.entity.OAuthClientDetails;
import com.elew.springauthserver.mapper.OAuthClientDetailsMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.stream.Collectors;

@Service
public class OAuthClientDetailsService implements ClientDetailsService {

    @Autowired
    private OAuthClientDetailsMapper oAuthClientDetailsMapper;


    /**
     * Load client details by client id.
     * @param clientId The client id.
     * @return
     * @throws ClientRegistrationException
     */
    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        QueryWrapper<OAuthClientDetails> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("client_id", clientId);
        OAuthClientDetails clientDetails = oAuthClientDetailsMapper.selectOne(queryWrapper);
        if (clientDetails == null) {
            throw new ClientRegistrationException("Client not found");
        }
        BaseClientDetails baseClientDetails = new BaseClientDetails();
        baseClientDetails.setResourceIds(Arrays.stream(clientDetails.getResourceIds().split(",")).map(String::trim).collect(Collectors.toList()));
        baseClientDetails.setClientId(clientDetails.getClientId());
        baseClientDetails.setClientSecret(clientDetails.getClientSecret());
        baseClientDetails.setScope(Arrays.asList(clientDetails.getScope().split(",")));
        baseClientDetails.setAuthorizedGrantTypes(Arrays.asList(clientDetails.getAuthorizedGrantTypes().split(",")));
        baseClientDetails.setRegisteredRedirectUri(
                clientDetails.getWebServerRedirectUri() != null ?
                        java.util.Collections.singleton(clientDetails.getWebServerRedirectUri()) :
                        java.util.Collections.emptySet()
        );
        baseClientDetails.setAuthorities(
                Arrays.stream(clientDetails.getAuthorities().split(","))
                        .map(auth -> new org.springframework.security.core.authority.SimpleGrantedAuthority(auth)).collect(Collectors.toList())
        );
        baseClientDetails.setAccessTokenValiditySeconds(clientDetails.getAccessTokenValidity());
        baseClientDetails.setRefreshTokenValiditySeconds(clientDetails.getRefreshTokenValidity());

        return baseClientDetails;
    }
}
package com.elew.springauthserver.service;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.elew.springauthserver.entity.Permission;
import com.elew.springauthserver.entity.Role;
import com.elew.springauthserver.entity.User;
import com.elew.springauthserver.mapper.RoleMapper;
import com.elew.springauthserver.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserMapper userMapper;
    private final RoleMapper roleMapper;

    public UserDetailsServiceImpl(UserMapper userMapper, RoleMapper roleMapper) {
        this.userMapper = userMapper;
        this.roleMapper = roleMapper;
    }


    /**
     * Load user details by username.
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username", username);
        User user = userMapper.selectOne(userQueryWrapper);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }

        Set<Role> roles = userMapper.selectRolesByUserId(user.getId());
        user.setRoles(roles);

        for (Role role : roles) {
            Set<Permission> permissions = roleMapper.selectPermissionsByRoleId(role.getId());
            role.setPermissions(permissions);
        }
        return user;
    }
}
8. 主工程类
package com.elew.springauthserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AuthServer {

    public static void main(String[] args) {
        SpringApplication.run(AuthServer.class, args);
    }
}
9. 数据库脚本
/*
 Navicat Premium Data Transfer

 Source Server         : 192.168.3.101
 Source Server Type    : MySQL
 Source Server Version : 80042
 Source Host           : 192.168.3.101:3306
 Source Schema         : spring_security

 Target Server Type    : MySQL
 Target Server Version : 80042
 File Encoding         : 65001

 Date: 10/07/2025 18:35:35
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(256) NOT NULL COMMENT '客户端唯一标识',
  `resource_ids` varchar(256) DEFAULT NULL COMMENT '客户端可访问的资源 ID 列表',
  `client_secret` varchar(256) DEFAULT NULL COMMENT '客户端密钥',
  `scope` varchar(256) DEFAULT NULL COMMENT '客户端权限范围',
  `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '客户端支持的授权类型',
  `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '授权回调地址',
  `authorities` varchar(256) DEFAULT NULL COMMENT '客户端拥有的权限',
  `access_token_validity` int DEFAULT NULL COMMENT '访问令牌有效期(秒)',
  `refresh_token_validity` int DEFAULT NULL COMMENT '刷新令牌有效期(秒)',
  `additional_information` text COMMENT '客户端额外信息,存储 JSON 数据',
  `autoapprove` varchar(256) DEFAULT NULL COMMENT '是否自动授权',
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OAuth2 客户端信息表';

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES ('app1', 'user,order', 'secret', 'read_profile,write_order', 'authorization_code,client_credentials,password,sms_code,refresh_token', 'http://localhost:8080/callback', 'ROLE_MOBILE_CLIENT', 3600, 2592000, '{\"device_type\": \"android\", \"app_version\": \"1.2.3\"}', 'false');
COMMIT;

-- ----------------------------
-- Table structure for permissions
-- ----------------------------
DROP TABLE IF EXISTS `permissions`;
CREATE TABLE `permissions` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限 ID',
  `permission_name` varchar(50) NOT NULL COMMENT '权限名称',
  `permission_code` varchar(50) NOT NULL COMMENT '权限编码',
  `description` varchar(255) DEFAULT NULL COMMENT '权限描述',
  `resource_type` varchar(20) DEFAULT NULL COMMENT '资源类型,如 MENU(菜单)、BUTTON(按钮)',
  `resource_url` varchar(255) DEFAULT NULL COMMENT '资源 URL',
  `status` tinyint DEFAULT '1' COMMENT '权限状态,1 正常,0 禁用',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `permission_name` (`permission_name`),
  UNIQUE KEY `permission_code` (`permission_code`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限表';

-- ----------------------------
-- Records of permissions
-- ----------------------------
BEGIN;
INSERT INTO `permissions` VALUES (1, '用户管理', 'USER_MANAGE', '管理用户信息', 'MENU', '/admin/users', 1, '2025-07-10 03:08:06', '2025-07-10 03:08:06');
INSERT INTO `permissions` VALUES (2, '角色管理', 'ROLE_MANAGE', '管理角色信息', 'MENU', '/admin/roles', 1, '2025-07-10 03:08:06', '2025-07-10 03:08:06');
INSERT INTO `permissions` VALUES (3, '权限管理', 'PERMISSION_MANAGE', '管理权限信息', 'MENU', '/admin/permissions', 1, '2025-07-10 03:08:06', '2025-07-10 03:08:06');
INSERT INTO `permissions` VALUES (4, '查看个人信息', 'USER_PROFILE_VIEW', '查看个人信息', 'BUTTON', '/user/profile', 1, '2025-07-10 03:08:06', '2025-07-10 03:08:06');
COMMIT;

-- ----------------------------
-- Table structure for role_permissions
-- ----------------------------
DROP TABLE IF EXISTS `role_permissions`;
CREATE TABLE `role_permissions` (
  `role_id` bigint NOT NULL COMMENT '角色 ID',
  `permission_id` bigint NOT NULL COMMENT '权限 ID',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色 - 权限关联表';

-- ----------------------------
-- Records of role_permissions
-- ----------------------------
BEGIN;
INSERT INTO `role_permissions` VALUES (1, 1, '2025-07-10 03:08:06');
INSERT INTO `role_permissions` VALUES (1, 2, '2025-07-10 03:08:06');
INSERT INTO `role_permissions` VALUES (1, 3, '2025-07-10 03:08:06');
INSERT INTO `role_permissions` VALUES (2, 4, '2025-07-10 03:08:06');
COMMIT;

-- ----------------------------
-- Table structure for roles
-- ----------------------------
DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色 ID',
  `role_name` varchar(50) NOT NULL COMMENT '角色名称',
  `role_code` varchar(50) NOT NULL COMMENT '角色编码',
  `description` varchar(255) DEFAULT NULL COMMENT '角色描述',
  `status` tinyint DEFAULT '1' COMMENT '角色状态,1 正常,0 禁用',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `role_name` (`role_name`),
  UNIQUE KEY `role_code` (`role_code`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色表';

-- ----------------------------
-- Records of roles
-- ----------------------------
BEGIN;
INSERT INTO `roles` VALUES (1, '超级管理员', 'ROLE_ADMIN', '拥有系统所有权限', 1, '2025-07-10 03:08:06', '2025-07-10 03:08:06');
INSERT INTO `roles` VALUES (2, '普通用户', 'ROLE_USER', '拥有基本的用户权限', 1, '2025-07-10 03:08:06', '2025-07-10 03:08:06');
COMMIT;

-- ----------------------------
-- Table structure for user_roles
-- ----------------------------
DROP TABLE IF EXISTS `user_roles`;
CREATE TABLE `user_roles` (
  `user_id` bigint NOT NULL COMMENT '用户 ID',
  `role_id` bigint NOT NULL COMMENT '角色 ID',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户 - 角色关联表';

-- ----------------------------
-- Records of user_roles
-- ----------------------------
BEGIN;
INSERT INTO `user_roles` VALUES (1, 1, '2025-07-10 03:08:06');
INSERT INTO `user_roles` VALUES (2, 2, '2025-07-10 03:08:06');
COMMIT;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户 ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) NOT NULL COMMENT '用户密码',
  `email` varchar(100) DEFAULT NULL COMMENT '用户邮箱',
  `phone` varchar(20) DEFAULT NULL COMMENT '用户手机号',
  `real_name` varchar(50) DEFAULT NULL COMMENT '用户真实姓名',
  `avatar` varchar(255) DEFAULT NULL COMMENT '用户头像地址',
  `status` tinyint DEFAULT '1' COMMENT '用户状态,1 正常,0 禁用',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';

-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (1, 'admin', '123456', 'admin@example.com', '13800138000', '超级管理员', 'https://example.com/admin_avatar.jpg', 1, '2025-07-10 03:08:06', '2025-07-10 03:23:16');
INSERT INTO `users` VALUES (2, 'user', '123456', 'user@example.com', '13900139000', '普通用户', 'https://example.com/user_avatar.jpg', 1, '2025-07-10 03:08:06', '2025-07-10 03:23:16');
INSERT INTO `users` VALUES (3, '13800138000', '123456', '13800138000@example.com', '13900139000', '普通用户', 'https://example.com/user_avatar.jpg', 1, '2025-07-10 03:08:06', '2025-07-10 03:23:16');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

至此,不出意外,项目应该可以正常启动了

紧接着赶紧测试吧,测试说明

-- 1. 获取授权码
    http://localhost:8080/oauth/authorize?response_type=code&client_id=app1&redirect_uri=http://localhost:8080/callback&scope=read_profile

    -- 授权码获取token
    http://localhost:8080/oauth/token
    form表单参数:
        grant_type=authorization_code
        code=your_authorization_code
        redirect_uri=http://localhost:8080/callback
    Authorization:
        Auth-Type: Basic Auth
        UserName: client, Password: secret


-- 2. 简化模式
    http://localhost:8080/oauth/authorize?response_type=token&client_id=app1&redirect_uri=http://localhost:8080/callback
    -- 返回
    http://localhost:8080/callback
        #access_token=TmPaqFkSm2HGVxaI1iSXwRfDdoQ
        &token_type=bearer
        &expires_in=2758
        &scope=read_profile%20write_order

-- 3. 密码模式
    http://localhost:8080/oauth/token
    form表单参数:
        grant_type=authorization_code
        code=your_authorization_code
        redirect_uri=http://localhost:8080/callback
    Authorization:
        Auth-Type: Basic Auth
        UserName: client, Password: secret

-- 4. 客户端模式
    http://localhost:8080/oauth/token
    form表单参数:
        grant_type=client_credentials
    Authorization:
        Auth-Type: Basic Auth
        UserName: client, Password: secret

-- 5. 刷新token
    http://localhost:8080/oauth/token
    form表单参数:
        grant_type=refresh_token
        refresh_token=your_refresh_token

-- 6. 自定义短信验证码

  http://localhost:8080/oauth/token
  form表单参数:
     grant_type=sms_code
     mobile=13800138000
     code=123456
  Authorization:
      Auth-Type: Basic Auth
      UserName: client, Password: secret

image.png

image.png

image.png