JWT + Spring Security / OAuth2.0:微服务统一登录、鉴权、单点登录全解析

在微服务架构中,服务被拆分为多个独立部署的节点,跨服务访问、用户身份统一管理成为核心痛点------用户在每个服务都需单独登录、权限无法统一管控、多系统切换频繁登录,这些问题不仅影响用户体验,更会带来严重的安全隐患。

一、微服务身份认证与鉴权的核心痛点

在单体应用中,我们通常通过Session存储用户身份信息,实现登录与鉴权,但这种方式在微服务架构中完全失效,核心痛点集中在3点:

1.1 Session共享问题

单体应用中,Session存储在服务器内存,微服务中多个服务部署在不同节点,Session无法跨服务共享,导致用户在A服务登录后,访问B服务仍需重新登录,体验极差。

1.2 权限管控分散

每个微服务单独维护一套权限规则,无法实现统一的角色、资源管控,不仅开发冗余,更易出现权限漏洞(如某服务遗漏权限校验、权限规则不一致)。

1.3 多系统单点登录需求

企业通常有多个关联系统(如电商系统、后台管理系统、APP接口),用户希望一次登录,即可访问所有授权系统,无需重复输入账号密码,这就需要单点登录(SSO)能力。

而JWT + Spring Security + OAuth2.0的组合,正是解决上述痛点的最优解:JWT实现无状态令牌传输,Spring Security实现权限管控,OAuth2.0实现授权与单点登录,三者协同,构建微服务统一身份认证与鉴权体系。

二、JWT、Spring Security、OAuth2.0

2.1 JWT:无状态令牌,解决Session共享难题

JWT(JSON Web Token)是一种轻量级的令牌规范,核心作用是在客户端与服务器之间安全地传输用户身份信息,采用无状态设计,无需在服务器存储Session,完美适配微服务架构。

2.1.1 JWT核心结构(3部分,用点号分隔)

JWT令牌由 Header(头部)Payload(载荷)Signature(签名) 三部分组成,示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE3MTUyODc2MDAsImlhdCI6MTcxNTI4NDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

  • Header(头部):指定JWT的签名算法和令牌类型,默认算法为HS256(HMAC SHA256),示例:

{

"alg": "HS256", // 签名算法

"typ": "JWT" // 令牌类型

}

  • Payload(载荷) :存储用户核心信息(如用户ID、用户名、角色)和令牌过期时间,分为标准声明自定义声明

  • { `` "userId": 1, // 自定义声明:用户ID `` "userName": "admin", // 自定义声明:用户名 `` "role": "ADMIN", // 自定义声明:角色 `` "exp": 1715287600, // 标准声明:过期时间 `` "iat": 1715284000 // 标准声明:签发时间 ``}

    • 标准声明(可选,但推荐使用):

    • sub:令牌面向的用户

    • iat:令牌签发时间

    • exp:令牌过期时间(时间戳,单位毫秒)

    • iss:令牌签发者

    • 自定义声明:根据业务需求添加,如用户ID(userId)、角色(role)、权限(permissions)等,注意:Payload不加密,不能存储敏感信息(如密码)。

  • Signature(签名) :核心安全保障,通过Header指定的算法,将Header(Base64编码)、Payload(Base64编码)和密钥(secret)进行加密,生成签名。服务器接收令牌后,会重新计算签名,若与令牌中的签名不一致,则说明令牌被篡改,直接拒绝访问。 签名计算公式:HMACSHA256( Base64Encode(Header) + "." + Base64Encode(Payload), secret )

2.1.2 JWT核心优势与注意事项
  • 优势

    • 无状态:服务器无需存储Session,仅通过令牌即可验证用户身份,减轻服务器压力,适配微服务集群部署;

    • 跨语言:基于JSON格式,支持所有语言(Java、Python、Go等),适配多端(Web、APP、小程序);

    • 自包含:Payload中包含用户核心信息,无需频繁查询数据库,提升接口响应速度;

    • 可扩展:支持自定义声明,适配不同业务场景的身份信息传输需求。

  • 注意事项

    • Payload不加密:严禁存储敏感信息(如密码、手机号),仅存储非敏感的用户标识和权限信息;

    • 密钥安全:签名密钥(secret)必须妥善保管,一旦泄露,攻击者可伪造令牌,引发安全风险;

    • 令牌过期:必须设置合理的过期时间(如1小时),过期后需重新登录获取新令牌;

    • 无法撤销:JWT令牌一旦签发,在过期前无法主动撤销(除非结合Redis黑名单机制)。

2.2 Spring Security:微服务权限管控核心框架

Spring Security是Spring生态中成熟的权限管理框架,核心作用是实现用户认证(登录校验)和授权(权限管控),提供了完善的安全防护机制(如CSRF防护、XSS防护、会话管理),可无缝整合JWT和OAuth2.0,是微服务权限管控的首选框架。

2.2.1 Spring Security核心概念
  • 认证(Authentication):验证用户身份的合法性(如账号密码是否正确),认证通过后,生成认证信息(Authentication对象),存储在SecurityContext中。

  • 授权(Authorization):验证用户是否拥有访问某个资源的权限(如普通用户能否访问管理员接口),核心是"资源-角色-用户"的关联关系。

  • SecurityContext :存储用户认证信息的上下文,线程安全,可通过SecurityContextHolder.getContext()获取当前登录用户信息。

  • UserDetailsService:核心接口,用于加载用户信息(如从数据库查询用户账号、密码、角色),是认证流程的核心组件。

  • PasswordEncoder:密码加密器,用于对用户密码进行加密存储(如BCrypt加密),避免明文存储密码,提升安全性。

  • FilterChain:安全过滤器链,Spring Security通过一系列过滤器(如UsernamePasswordAuthenticationFilter、JwtAuthenticationFilter)处理请求,完成认证和授权。

2.2.2 Spring Security核心流程(认证+授权)
  1. 用户发起登录请求(如POST /login),携带账号密码;

  2. UsernamePasswordAuthenticationFilter拦截请求,将账号密码封装为Authentication对象;

  3. 调用AuthenticationManager(认证管理器),触发认证流程;

  4. AuthenticationManager调用UserDetailsService,加载数据库中的用户信息(UserDetails);

  5. PasswordEncoder对比用户提交的密码与数据库中加密后的密码,验证是否一致;

  6. 认证通过:生成包含用户信息和权限的Authentication对象,存入SecurityContext;

  7. 认证失败:抛出异常,返回登录失败提示;

  8. 用户访问受保护资源时,FilterSecurityInterceptor拦截请求,校验当前用户是否拥有该资源的访问权限;

  9. 授权通过:允许访问资源;授权失败:返回403 Forbidden。

2.3 OAuth2.0:授权协议,实现单点登录与第三方授权

OAuth2.0是一种开放的授权协议,核心作用是实现"第三方授权"和"单点登录(SSO)",允许用户通过一个账号(如微信、QQ)登录多个关联系统,无需重复注册和登录,同时避免用户将核心账号密码泄露给第三方系统。

注意:OAuth2.0是授权协议,不是认证协议,它的核心是"授权"------用户授权第三方系统访问自己的资源(如微信授权某APP获取用户昵称、头像),而认证是验证用户身份的过程(如微信登录时验证账号密码)。

2.3.1 OAuth2.0核心角色
  • 资源所有者(Resource Owner):用户,拥有资源的所有权(如微信用户拥有自己的昵称、头像等资源)。

  • 客户端(Client):需要获取用户资源的应用(如某APP、某网站),需提前在授权服务器注册,获取客户端ID(client_id)和客户端密钥(client_secret)。

  • 授权服务器(Authorization Server):负责验证用户身份、颁发授权令牌(如access_token),是OAuth2.0的核心组件(如微信授权服务器)。

  • 资源服务器(Resource Server):存储用户资源的服务器(如微信的用户信息服务器),客户端通过授权令牌访问资源服务器,获取用户资源。

2.3.2 OAuth2.0核心授权流程(通用流程)
  1. 客户端(APP)引导用户跳转到授权服务器,请求用户授权;

  2. 用户验证身份(如登录微信),并同意授权客户端访问自己的资源;

  3. 授权服务器颁发**授权码(code)**给客户端;

  4. 客户端携带授权码(code)、客户端ID、客户端密钥,向授权服务器请求访问令牌(access_token)

  5. 授权服务器验证信息无误后,颁发access_token(访问令牌)和refresh_token(刷新令牌)给客户端;

  6. 客户端携带access_token,向资源服务器请求访问用户资源;

  7. 资源服务器验证access_token的合法性,验证通过后,返回用户资源给客户端。

2.3.3 OAuth2.0 4种授权模式(重点掌握2种)

OAuth2.0提供4种授权模式,适配不同的业务场景,其中授权码模式密码模式是微服务中最常用的两种。

  • 授权码模式(Authorization Code)

    • 特点:最安全、最常用的模式,通过授权码获取access_token,避免直接传递账号密码,适合Web应用、APP等场景;

    • 适用场景:单点登录(SSO)、第三方授权(如APP用微信登录);

    • 核心优势:安全性高,授权码仅短期有效,且客户端无需存储用户账号密码。

  • 密码模式(Password)

    • 特点:用户直接向客户端提供账号密码,客户端携带账号密码向授权服务器请求access_token;

    • 适用场景:微服务内部系统(如后台管理系统),客户端与授权服务器属于同一信任体系,且用户信任客户端;

    • 注意:安全性较低,仅适用于内部信任场景,严禁用于第三方授权。

  • 简化模式(Implicit):无需授权码,直接颁发access_token,安全性低,仅适用于纯前端应用(如Vue、React),不推荐生产使用。

  • 客户端凭证模式(Client Credentials):客户端通过自身的client_id和client_secret获取access_token,无需用户参与,适用于服务间通信(如微服务A调用微服务B)。

2.3.4 OAuth2.0核心令牌
  • access_token(访问令牌):用于访问资源服务器的令牌,短期有效(如1小时),过期后需重新获取;

  • refresh_token(刷新令牌):用于在access_token过期后,无需重新登录,直接获取新的access_token,长期有效(如7天);

  • 授权码(code):用于获取access_token的临时凭证,短期有效(如5分钟),一次使用后失效。

三、实操落地:Spring Security + JWT 实现微服务统一登录与鉴权

先实现最基础的"统一登录与鉴权":基于Spring Security + JWT,实现用户登录生成JWT令牌,后续请求携带令牌完成身份验证和权限管控,适配微服务架构(无状态)。

3.1 第一步:导入依赖(Maven)

XML 复制代码
<!-- Spring Boot Web -->
<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>

<!-- JWT依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

<!-- 数据库依赖(模拟用户数据,可替换为MySQL) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

3.2 第二步:配置JWT工具类(生成令牌、验证令牌)

核心工具类,负责JWT令牌的生成、解析、验证,封装通用方法,便于后续调用。

java 复制代码
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenUtil {

    // JWT签名密钥(生产环境需配置在配置中心,如Nacos,严禁硬编码)
    @Value("${jwt.secret}")
    private String secret;

    // JWT过期时间(单位:毫秒,此处配置1小时)
    @Value("${jwt.expiration}")
    private long expiration;

    // 从令牌中获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    // 从令牌中获取过期时间
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    // 从令牌中获取自定义声明
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    // 解析令牌,获取所有声明(需验证签名)
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secret.getBytes())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    // 判断令牌是否过期
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    // 生成JWT令牌(基于用户信息)
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        // 自定义声明:添加用户角色(可根据需求添加更多信息)
        claims.put("roles", userDetails.getAuthorities());
        return doGenerateToken(claims, userDetails.getUsername());
    }

    // 生成令牌核心方法
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims) // 自定义声明
                .setSubject(subject) // 用户名(唯一标识)
                .setIssuedAt(new Date(System.currentTimeMillis())) // 签发时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration)) // 过期时间
                .signWith(SignatureAlgorithm.HS256, secret.getBytes()) // 签名算法+密钥
                .compact();
    }

    // 验证令牌(验证签名、过期时间、用户名匹配)
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

3.3 第三步:配置Spring Security核心配置

核心配置类,用于配置认证流程、授权规则、JWT过滤器等,替代默认的Session认证。

java 复制代码
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.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // 启用Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限控制
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    // 密码加密器(BCrypt加密,不可逆,安全性高)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 认证提供者(关联UserDetailsService和PasswordEncoder)
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    // 认证管理器(核心认证组件)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    // 核心安全配置(配置授权规则、过滤器、会话管理等)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF防护(微服务中JWT无状态,无需CSRF)
                .csrf().disable()
                // 配置未认证请求的处理方式(返回401)
                .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
                // 配置会话管理:无状态(不创建Session)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 配置授权规则
                .authorizeRequests()
                // 登录接口、注册接口允许匿名访问
                .antMatchers("/api/auth/login", "/api/auth/register").permitAll()
                // 静态资源允许匿名访问
                .antMatchers("/static/**", "/swagger-ui/**").permitAll()
                // 管理员接口仅允许ADMIN角色访问
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                // 普通用户接口允许USER或ADMIN角色访问
                .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                // 其他所有请求都需要认证
                .anyRequest().authenticated();

        // 注册认证提供者
        http.authenticationProvider(authenticationProvider());

        // 添加JWT过滤器(在用户名密码过滤器之前执行,先验证令牌)
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

3.4 第四步:实现JWT过滤器与认证异常处理

JWT过滤器负责拦截所有请求,提取请求头中的JWT令牌,验证令牌合法性,若验证通过,将用户信息存入SecurityContext,实现无状态认证。

4.1 JWT过滤器(JwtAuthenticationFilter)
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    // 拦截请求,验证JWT令牌
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        try {
            // 1. 从请求头中提取JWT令牌(请求头格式:Authorization: Bearer <token>)
            String jwt = getJwtFromRequest(request);

            // 2. 验证令牌是否存在且有效
            if (jwt != null && !jwt.isEmpty() && jwtTokenUtil.validateToken(jwt, userDetailsService.loadUserByUsername(jwtTokenUtil.getUsernameFromToken(jwt)))) {
                // 3. 从令牌中获取用户名,加载用户信息
                String username = jwtTokenUtil.getUsernameFromToken(jwt);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                // 4. 创建认证对象,存入SecurityContext
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("无法设置用户认证信息: {}", e);
        }

        // 继续执行过滤器链
        filterChain.doFilter(request, response);
    }

    // 从请求头中提取JWT令牌
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7); // 截取Bearer后面的令牌部分
        }
        return null;
    }
}
4.2 认证异常处理(JwtAuthenticationEntryPoint)

当令牌无效、过期或未携带令牌时,返回统一的401响应,替代默认的登录页面。

java 复制代码
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    // 未认证请求的处理逻辑
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        // 设置响应状态码401,返回JSON格式的错误信息
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":401,\"message\":\"未认证,请先登录获取令牌\"}");
    }
}

3.5 第五步:实现UserDetailsService(加载用户信息)

自定义UserDetailsService,从数据库中加载用户账号、密码、角色信息,适配Spring Security的认证流程。

5.1 实体类(User)
java 复制代码
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Data
@Entity
@Table(name = "sys_user")
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username; // 用户名(唯一)

    @Column(nullable = false)
    private String password; // 加密后的密码

    private String role; // 角色(如ADMIN、USER)

    // 实现UserDetails接口方法:获取用户权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 将角色转换为Spring Security认可的权限格式(ROLE_前缀)
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        return authorities;
    }

    // 实现UserDetails接口方法:账号是否未过期(默认true)
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 实现UserDetails接口方法:账号是否未锁定(默认true)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 实现UserDetails接口方法:凭证是否未过期(默认true)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 实现UserDetails接口方法:账号是否启用(默认true)
    @Override
    public boolean isEnabled() {
        return true;
    }
}
5.2 UserRepository(数据库操作)
java 复制代码
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // 根据用户名查询用户(Spring Security认证核心方法)
    Optional<User> findByUsername(String username);
}
5.3 自定义UserDetailsService实现
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
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;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    // 加载用户信息(根据用户名)
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库查询用户,若不存在则抛出异常
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));

        // 返回User对象(已实现UserDetails接口)
        return user;
    }
}

3.6 第六步:实现登录接口(生成JWT令牌)

自定义登录接口,接收用户账号密码,完成认证后,生成JWT令牌并返回给客户端。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 登录接口:接收账号密码,返回JWT令牌
    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest loginRequest) {
        // 1. 执行认证(验证账号密码)
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );

        // 2. 认证通过,将认证信息存入SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // 3. 加载用户信息,生成JWT令牌
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
        String jwt = jwtTokenUtil.generateToken(userDetails);

        // 4. 返回令牌和用户信息
        Map<String, String> response = new HashMap<>();
        response.put("token", jwt);
        response.put("username", userDetails.getUsername());
        response.put("role", userDetails.getAuthorities().iterator().next().getAuthority().replace("ROLE_", ""));

        return ResponseEntity.ok(response);
    }

    // 注册接口(可选,用于测试)
    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody RegisterRequest registerRequest) {
        // 检查用户名是否已存在
        if (userRepository.findByUsername(registerRequest.getUsername()).isPresent()) {
            return ResponseEntity.badRequest().body("用户名已存在");
        }

        // 创建用户,加密密码
        User user = new User();
        user.setUsername(registerRequest.getUsername());
        user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
        user.setRole(registerRequest.getRole()); // 如"USER"、"ADMIN"
        userRepository.save(user);

        return ResponseEntity.ok("注册成功");
    }

    // 登录请求参数封装
    public static class LoginRequest {
        private String username;
        private String password;
        // getter/setter
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }

    // 注册请求参数封装
    public static class RegisterRequest {
        private String username;
        private String password;
        private String role;
        // getter/setter
        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
        public String getRole() { return role; }
        public void setRole(String role) { this.role = role; }
    }
}

3.7 第七步:配置文件(application.yml)

XML 复制代码
spring:
  # 数据库配置(H2内存数据库,用于测试)
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 123456
  # JPA配置
  jpa:
    hibernate:
      ddl-auto: update # 自动创建表结构
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  # H2控制台配置(访问:http://localhost:8080/h2-console)
  h2:
    console:
      enabled: true
      path: /h2-console

# JWT配置
jwt:
  secret: abc1234567890abc1234567890abc1234 # 签名密钥(生产环境需修改,建议至少32位)
  expiration: 3600000 # 过期时间(1小时,单位:毫秒)

# 服务器端口
server:
  port: 8080

3.8 测试验证

  1. 启动项目,访问 http://localhost:8080/h2-console,登录H2数据库,插入测试用户:

    sql 复制代码
     INSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密码:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密码:123456
  2. 调用注册接口(可选):POST http://localhost:8080/api/auth/register,请求体: { `` "username": "test", `` "password": "123456", `` "role": "USER" ``}

  3. 调用登录接口:POST http://localhost:8080/api/auth/login,请求体: { `` "username": "admin", `` "password": "123456" ``}响应结果(包含JWT令牌):{ `` "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9XSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTcxNTI5MTYwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7", `` "username": "admin", `` "role": "ADMIN" ``}

  4. 访问受保护接口(如管理员接口):GET http://localhost:8080/api/admin/test ,请求头添加Authorization: Bearer 令牌,即可正常访问;若不携带令牌或令牌无效,返回401。

四、OAuth2.0 + JWT + Spring Security 实现单点登录(SSO)

前面实现了单个微服务的登录与鉴权,而微服务架构中通常有多个服务(如订单服务、用户服务、后台管理服务),需要实现"单点登录"------用户一次登录,即可访问所有授权服务。

核心方案:基于 OAuth2.0 授权码模式,搭建独立的授权服务器 (统一处理登录、颁发令牌)和资源服务器(各微服务),结合JWT实现无状态单点登录。

4.1 架构设计(核心组件)

  • 授权服务器(Authorization Server):独立部署,负责用户认证、颁发JWT令牌(access_token、refresh_token)、处理授权请求,是单点登录的核心。

  • 资源服务器(Resource Server):各个微服务(如订单服务、用户服务),配置OAuth2.0和JWT,验证令牌合法性,实现权限管控。

  • 客户端(Client):需要接入单点登录的应用(如Web后台、APP、小程序),提前在授权服务器注册。

4.2 第一步:搭建授权服务器(Authorization Server)

基于Spring Security OAuth2.0,搭建独立的授权服务器,实现用户登录、授权码颁发、JWT令牌生成。

2.1 导入依赖(Maven)
XML 复制代码
<!-- 新增OAuth2.0授权服务器依赖 -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.8.RELEASE</version>
</dependency>

<!-- 其他依赖(Spring Boot Web、Spring Security、JWT、数据库)同上 -->
2.2 配置授权服务器(AuthorizationServerConfig)
java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
@EnableAuthorizationServer // 启用授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    // 客户端ID(提前注册,用于客户端身份验证)
    private static final String CLIENT_ID = "client1";
    // 客户端密钥(加密存储,密码:123456)
    private static final String CLIENT_SECRET = "$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6";
    // 授权范围
    private static final String SCOPE = "all";
    // 授权码模式
    private static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
    // 密码模式
    private static final String GRANT_TYPE_PASSWORD = "password";
    // 刷新令牌模式
    private static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
    // 令牌有效期(1小时)
    private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 3600;
    // 刷新令牌有效期(7天)
    private static final int REFRESH_TOKEN_VALIDITY_SECONDS = 604800;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private PasswordEncoder passwordEncoder;

    // JWT签名密钥(与资源服务器、JWT工具类一致,生产环境配置在配置中心)
    private static final String JWT_SECRET = "abc1234567890abc1234567890abc1234";

    // 配置令牌存储(JWT),用于存储和解析JWT令牌
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    // 配置JWT令牌转换器(设置签名密钥,确保令牌生成和验证的一致性)
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(JWT_SECRET); // 与JWT工具类的密钥完全一致
        return converter;
    }

    // 配置客户端信息(客户端在授权服务器注册的核心信息,用于客户端身份校验)
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                // 客户端ID(唯一标识,客户端需携带此ID请求授权)
                .withClient(CLIENT_ID)
                // 客户端密钥(加密存储,客户端请求时需携带加密前的密钥进行校验)
                .secret(CLIENT_SECRET)
                // 授权范围,用于限制客户端可访问的资源范围
                .scopes(SCOPE)
                // 支持的授权模式,此处兼容授权码模式(SSO核心)、密码模式(内部系统)、刷新令牌模式
                .authorizedGrantTypes(GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_PASSWORD, GRANT_TYPE_REFRESH_TOKEN)
                // 访问令牌有效期(1小时,避免令牌长期有效带来的安全风险)
                .accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS)
                // 刷新令牌有效期(7天,用户无需频繁登录,提升体验)
                .refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS)
                // 回调地址(授权码模式必填,授权服务器颁发授权码后,跳转至此地址传递授权码)
                .redirectUris("http://localhost:8081/callback");
    }

    // 配置授权服务器端点(核心组件关联,确保认证和令牌生成流程正常)
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                // 关联认证管理器(用于密码模式,验证用户账号密码合法性)
                .authenticationManager(authenticationManager)
                // 关联令牌存储(JWT),用于存储和读取令牌信息
                .tokenStore(tokenStore())
                // 关联令牌转换器(JWT),用于生成和解析JWT令牌
                .accessTokenConverter(accessTokenConverter());
    }

    // 配置授权服务器安全规则(控制授权服务器端点的访问权限)
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 允许所有客户端访问token_key端点(获取JWT签名公钥,用于资源服务器验证令牌)
                .tokenKeyAccess("permitAll()")
                // 允许所有客户端访问check_token端点(验证令牌的合法性,资源服务器会调用此端点)
                .checkTokenAccess("permitAll()")
                // 允许客户端通过表单认证(用于客户端身份校验,简化客户端请求流程)
                .allowFormAuthenticationForClients();
    }
}

4.3 授权服务器配套配置(完善认证流程)

授权服务器需依赖前文实现的UserDetailsService、PasswordEncoder等组件,同时补充Spring Security配置(避免默认登录页面干扰),确保认证流程正常。

4.3.1 授权服务器Spring Security配置(SecurityConfig)
java 复制代码
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.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    // 密码加密器(与前文一致,BCrypt不可逆加密,确保密码安全)
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 认证提供者(关联UserDetailsService和PasswordEncoder,用于加载用户信息并校验密码)
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    // 认证管理器(核心认证组件,授权服务器密码模式需依赖此组件)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    // 安全过滤器链配置(关闭Session,允许授权相关端点匿名访问)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF防护(授权服务器无状态,无需CSRF)
                .csrf().disable()
                // 关闭Session(授权服务器无需存储用户会话,适配微服务无状态架构)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 授权规则配置
                .authorizeRequests()
                // 授权服务器核心端点允许匿名访问(客户端请求授权、获取令牌需访问这些端点)
                .antMatchers("/oauth/authorize", "/oauth/token", "/oauth/check_token", "/oauth/token_key").permitAll()
                // 登录接口、注册接口允许匿名访问(用于用户注册和登录验证)
                .antMatchers("/api/auth/login", "/api/auth/register").permitAll()
                // 其他所有请求需认证(避免未授权访问)
                .anyRequest().authenticated();

        // 注册认证提供者
        http.authenticationProvider(authenticationProvider());

        return http.build();
    }
}
4.3.2 授权服务器配置文件(application.yml)

配置端口、数据库、JWT等信息,与前文保持一致,确保组件协同工作:

XML 复制代码
spring:
  # 数据库配置(H2内存数据库,用于测试,生产环境替换为MySQL)
  datasource:
    url: jdbc:h2:mem:authdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 123456
  # JPA配置(自动创建表结构,简化测试)
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true
  # H2控制台配置(访问:http://localhost:8080/h2-console,用于插入测试用户)
  h2:
    console:
      enabled: true
      path: /h2-console

# JWT配置(与资源服务器、令牌转换器一致)
jwt:
  secret: abc1234567890abc1234567890abc1234 # 签名密钥,生产环境需修改并配置在配置中心
  expiration: 3600000 # 访问令牌有效期(1小时,与授权服务器配置一致)

# 服务器端口(授权服务器独立部署,端口设为8080,避免与资源服务器冲突)
server:
  port: 8080

4.4 授权服务器测试验证

启动授权服务器,完成以下测试,确保授权服务器可正常颁发授权码和JWT令牌:

  1. 启动项目,访问H2控制台**(http://localhost:8080/h2-console)**,登录后插入测试用户(与前文一致):

    sql 复制代码
    INSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密码:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密码:123456
  2. 请求授权码(授权码模式):访问以下地址,引导用户登录并授权,获取授权码(code): http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/callback&scope=all

    1. 访问后会跳转至登录页面,输入用户名(admin)和密码(123456);

    2. 登录成功后,会跳转至回调地址(http://localhost:8081/callback),地址栏会携带授权码(code参数),例如:**`http://localhost:8081/callback?code=abc123`**(code为临时凭证,5分钟内有效)。

  3. 通过授权码获取访问令牌(access_token):使用Postman发送POST请求,地址:http://localhost:8080/oauth/token,参数如下:

    1. 请求头:添加**Authorization: Basic Y2xpZW50MToxMjM0NTY=**(client1:123456的Base64编码);

    2. 请求体(form-data): grant_type=authorization_codecode=步骤2获取的授权码redirect_uri=http://localhost:8081/callbackscope=all

    3. 响应结果:会返回access_token(JWT令牌)、refresh_token、token_type等信息,示例:{ `` "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjoidXNlciIsImV4cCI6MTcxNTI5MTYwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7", `` "token_type": "bearer", `` "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjoidXNlciIsImF0aSI6ImFiYzEyMyIsImV4cCI6MTcxNTg5MjgwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8Z8", `` "expires_in": 3599, `` "scope": "all" ``}

五、第二步:搭建资源服务器(Resource Server)

资源服务器即各个微服务(如订单服务、用户服务),需配置OAuth2.0和JWT,实现令牌验证和权限管控,确保只有携带合法JWT令牌的请求才能访问受保护资源。

5.1 导入资源服务器依赖(Maven)

XML 复制代码
<!-- Spring Boot Web -->
<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.0资源服务器依赖 -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.8.RELEASE</version>
</dependency>

<!-- JWT依赖(与授权服务器一致) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

5.2 配置资源服务器(ResourceServerConfig)

核心配置:关联JWT令牌转换器和令牌存储,配置资源访问权限,验证令牌合法性。

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
@EnableResourceServer // 启用资源服务器
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    // 资源ID(唯一标识,与授权服务器客户端配置的资源范围对应)
    private static final String RESOURCE_ID = "all";

    // JWT签名密钥(与授权服务器完全一致,否则无法验证令牌)
    private static final String JWT_SECRET = "abc1234567890abc1234567890abc1234";

    // 配置令牌存储(JWT),与授权服务器一致
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    // 配置JWT令牌转换器(设置签名密钥,用于验证令牌签名)
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(JWT_SECRET);
        return converter;
    }

    // 配置资源服务器核心信息(令牌存储、资源ID)
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                // 关联资源ID(与授权服务器客户端的scope对应)
                .resourceId(RESOURCE_ID)
                // 关联令牌存储(用于验证令牌合法性)
                .tokenStore(tokenStore())
                // 令牌验证失败时,返回401未授权响应
                .stateless(true);
    }

    // 配置资源访问权限规则(根据用户角色控制资源访问)
    @Override
    public void configure(org.springframework.security.config.annotation.web.builders.HttpSecurity http) throws Exception {
        http
                // 关闭CSRF防护(资源服务器无状态,无需CSRF)
                .csrf().disable()
                // 关闭Session(适配微服务无状态架构)
                .sessionManagement().sessionCreationPolicy(org.springframework.security.config.http.SessionCreationPolicy.STATELESS).and()
                // 授权规则配置
                .authorizeRequests()
                // 公开接口允许匿名访问(如健康检查接口)
                .antMatchers("/health/**").permitAll()
                // 管理员接口仅允许ADMIN角色访问
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                // 普通用户接口允许USER或ADMIN角色访问
                .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                // 其他所有资源需携带合法令牌才能访问
                .anyRequest().authenticated();
    }
}

5.3 资源服务器配置文件(application.yml)

配置端口(与授权服务器不同,避免端口冲突)、JWT等信息:

XML 复制代码
# 服务器端口(资源服务器独立部署,设为8081,与授权服务器8080区分)
server:
  port: 8081

# JWT配置(与授权服务器完全一致)
jwt:
  secret: abc1234567890abc1234567890abc1234
  expiration: 3600000

# OAuth2.0资源服务器配置
security:
  oauth2:
    resource:
      # 令牌验证端点(授权服务器的check_token端点,用于验证令牌合法性)
      token-info-uri: http://localhost:8080/oauth/check_token
      # 资源ID(与资源服务器配置的RESOURCE_ID一致)
      id: all

5.4 实现资源服务器测试接口

创建测试接口,用于验证单点登录和权限管控是否生效:

java 复制代码
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class TestController {

    // 普通用户接口(USER、ADMIN角色可访问)
    @GetMapping("/user/test")
    public String userTest() {
        // 获取当前登录用户信息
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return "普通用户接口访问成功!当前登录用户:" + userDetails.getUsername() + ",角色:" + userDetails.getAuthorities();
    }

    // 管理员接口(仅ADMIN角色可访问)
    @GetMapping("/admin/test")
    public String adminTest() {
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return "管理员接口访问成功!当前登录用户:" + userDetails.getUsername() + ",角色:" + userDetails.getAuthorities();
    }

    // 回调接口(用于接收授权服务器颁发的授权码,仅授权码模式使用)
    @GetMapping("/callback")
    public String callback(String code) {
        // 此处可接收授权码,后续可结合客户端逻辑获取access_token(实际场景中由客户端处理)
        return "授权码接收成功!code:" + code;
    }
}

六、第三步:单点登录(SSO)完整测试

启动授权服务器(8080端口)和资源服务器(8081端口),完成以下测试,验证单点登录效果(一次登录,多服务访问):

6.1 测试流程(授权码模式,SSO核心流程)

  1. 获取授权码:访问**http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/callback&scope=all**,登录admin用户(密码123456),获取回调地址中的code;

  2. 获取access_token:通过Postman发送POST请求到http://localhost:8080/oauth/token,携带code、client_id、client_secret等参数,获取access_token(JWT令牌);

  3. 访问资源服务器接口:

    1. 访问普通用户接口:http://localhost:8081/api/user/test,请求头添加Authorization: Bearer 第一步获取的access_token,可正常访问;

    2. 访问管理员接口:http://localhost:8081/api/admin/test,请求头添加相同的access_token,可正常访问(admin角色拥有权限);

  4. 测试单点登录:新增另一个资源服务器(如订单服务,端口8082),配置与8081端口资源服务器一致,使用相同的access_token访问其受保护接口,无需重新登录即可正常访问,实现单点登录。

6.2 常见问题排查

  • 令牌验证失败:检查资源服务器与授权服务器的JWT_SECRET是否一致,access_token是否过期;

  • 权限不足(403):检查用户角色是否与接口要求的角色匹配,User实体类中角色转换是否添加ROLE_前缀;

  • 授权码无效:授权码仅5分钟有效,且一次使用后失效,需重新获取授权码。

七、生产环境适配

上述实操为测试环境配置,生产环境需进行以下优化,提升安全性和可维护性:

7.1 安全优化

  • 密钥管理:JWT_SECRET、client_secret等敏感信息,不硬编码,配置在Nacos、Apollo等配置中心,定期更换;

  • 令牌安全:缩短access_token有效期(如30分钟),refresh_token添加黑名单机制(结合Redis),支持主动注销令牌;

  • 加密传输:所有接口使用HTTPS协议,避免令牌在传输过程中被窃取;

  • 权限细化:基于资源的细粒度权限管控(如用户只能访问自己的订单),结合Spring Security的方法级权限注解(@PreAuthorize)。

7.2 架构优化

  • 授权服务器集群部署:避免单点故障,使用Redis共享令牌黑名单;

  • 资源服务器统一配置:将OAuth2.0和JWT配置抽取为公共依赖,所有微服务引入,减少重复开发;

  • 日志与监控:添加令牌生成、验证、失效的日志记录,监控令牌使用情况,及时发现异常访问。

八、总结

  1. 授权服务器:独立部署,负责用户认证、颁发授权码和JWT令牌,统一管理客户端和用户信息;

  2. 资源服务器:各个微服务,配置令牌验证规则,实现权限管控,仅允许携带合法令牌的请求访问;

  3. 单点登录:用户通过授权服务器一次登录,获取JWT令牌,即可访问所有授权的资源服务器,无需重复登录。

相关推荐
rchmin2 分钟前
向量数据库Milvus安装及使用实战经验分享
数据库·milvus
ego.iblacat8 分钟前
Python 连接 MySQL 数据库
数据库·python·mysql
祖传F8719 分钟前
quickbi数据集数据查询时间字段显示正确,仪表板不显示
数据库·sql·阿里云
Leon-Ning Liu39 分钟前
Oracle 26ai新特性:时区、表空间、审计方面的新特性
数据库·oracle
humors2211 小时前
各厂商工具包网址
java·数据库·python·华为·sdk·苹果·工具包
Yushan Bai1 小时前
ORACLE数据库在进行DROP TABLE时失败报错ORA-00604问题的分析处理
数据库·oracle
77美式2 小时前
Node + Express + MongoDB 后端部署全解析:新手零踩坑
数据库·mongodb·express
城数派2 小时前
2000-2025年我国省市县三级逐8天日间地表温度数据(Shp/Excel格式)
数据库·arcgis·信息可视化·数据分析·excel
AC赳赳老秦2 小时前
OpenClaw text-translate技能:多语言批量翻译,解决跨境工作沟通难题
大数据·运维·数据库·人工智能·python·deepseek·openclaw
AI应用实战 | RE2 小时前
014、索引高级实战:当单一向量库不够用的时候
数据库·人工智能·langchain