java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Security 微服务统一登录

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

作者:鬼先生_sir

本文介绍了微服务架构中基于JWT、SpringSecurity和OAuth2.0实现统一身份认证与鉴权的方法,接着详细介绍了JWT、SpringSecurity和OAuth2.0的工作原理和核心概念,然后通过实操步骤展示了如何在Spring Boot中实现微服务统一登录与鉴权

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

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

在单体应用中,我们通常通过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" // 令牌类型
}
{ "userId": 1, // 自定义声明:用户ID "userName": "admin", // 自定义声明:用户名 "role": "ADMIN", // 自定义声明:角色 "exp": 1715287600, // 标准声明:过期时间 "iat": 1715284000 // 标准声明:签发时间 }

2.1.2 JWT核心优势与注意事项

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

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

2.2.1 Spring Security核心概念

2.2.2 Spring Security核心流程(认证+授权)

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

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

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

2.3.1 OAuth2.0核心角色

2.3.2 OAuth2.0核心授权流程(通用流程)

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

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

2.3.4 OAuth2.0核心令牌

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

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

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

<!-- 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令牌的生成、解析、验证,封装通用方法,便于后续调用。

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认证。

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)

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响应,替代默认的登录页面。

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)

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(数据库操作)

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实现

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令牌并返回给客户端。

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)

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 测试验证

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

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

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

4.1 架构设计(核心组件)

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

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

2.1 导入依赖(Maven)

<!-- 新增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)

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)

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等信息,与前文保持一致,确保组件协同工作:

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令牌:

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

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

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

<!-- 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令牌转换器和令牌存储,配置资源访问权限,验证令牌合法性。

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等信息:

# 服务器端口(资源服务器独立部署,设为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 实现资源服务器测试接口

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

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核心流程)

6.2 常见问题排查

七、生产环境适配

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

7.1 安全优化

7.2 架构优化

八、总结

到此这篇关于JWT + Spring Security / OAuth2.0:微服务统一登录、鉴权、单点登录全解析的文章就介绍到这了,更多相关Spring Security 微服务统一登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文