
在微服务架构中,服务被拆分为多个独立部署的节点,跨服务访问、用户身份统一管理成为核心痛点------用户在每个服务都需单独登录、权限无法统一管控、多系统切换频繁登录,这些问题不仅影响用户体验,更会带来严重的安全隐患。
一、微服务身份认证与鉴权的核心痛点
在单体应用中,我们通常通过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核心流程(认证+授权)
-
用户发起登录请求(如POST /login),携带账号密码;
-
UsernamePasswordAuthenticationFilter拦截请求,将账号密码封装为Authentication对象;
-
调用AuthenticationManager(认证管理器),触发认证流程;
-
AuthenticationManager调用UserDetailsService,加载数据库中的用户信息(UserDetails);
-
PasswordEncoder对比用户提交的密码与数据库中加密后的密码,验证是否一致;
-
认证通过:生成包含用户信息和权限的Authentication对象,存入SecurityContext;
-
认证失败:抛出异常,返回登录失败提示;
-
用户访问受保护资源时,FilterSecurityInterceptor拦截请求,校验当前用户是否拥有该资源的访问权限;
-
授权通过:允许访问资源;授权失败:返回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核心授权流程(通用流程)
-
客户端(APP)引导用户跳转到授权服务器,请求用户授权;
-
用户验证身份(如登录微信),并同意授权客户端访问自己的资源;
-
授权服务器颁发**授权码(code)**给客户端;
-
客户端携带授权码(code)、客户端ID、客户端密钥,向授权服务器请求访问令牌(access_token);
-
授权服务器验证信息无误后,颁发access_token(访问令牌)和refresh_token(刷新令牌)给客户端;
-
客户端携带access_token,向资源服务器请求访问用户资源;
-
资源服务器验证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 测试验证
-
启动项目,访问
http://localhost:8080/h2-console,登录H2数据库,插入测试用户:sqlINSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密码:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密码:123456 -
调用注册接口(可选):POST
http://localhost:8080/api/auth/register,请求体:{ `` "username": "test", `` "password": "123456", `` "role": "USER" ``} -
调用登录接口:POST
http://localhost:8080/api/auth/login,请求体:{ `` "username": "admin", `` "password": "123456" ``}响应结果(包含JWT令牌):{ `` "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9XSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTcxNTI5MTYwMCwiaWF0IjoxNzE1Mjg4MDAwfQ.7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7Z7", `` "username": "admin", `` "role": "ADMIN" ``} -
访问受保护接口(如管理员接口):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令牌:
-
启动项目,访问H2控制台**(http://localhost:8080/h2-console)**,登录后插入测试用户(与前文一致):
sqlINSERT INTO sys_user (username, password, role) VALUES ('admin', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'ADMIN'), -- 密码:123456 ('user', '$2a$10$EixZaYbB.rK4fl8x2q7Meu6Q6D2V4Xw6Q6D2V4Xw6Q6D2V4Xw6Q6', 'USER'); -- 密码:123456 -
请求授权码(授权码模式):访问以下地址,引导用户登录并授权,获取授权码(code):
http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/callback&scope=all-
访问后会跳转至登录页面,输入用户名(admin)和密码(123456);
-
登录成功后,会跳转至回调地址(http://localhost:8081/callback),地址栏会携带授权码(code参数),例如:**`http://localhost:8081/callback?code=abc123`**(code为临时凭证,5分钟内有效)。
-
-
通过授权码获取访问令牌(access_token):使用Postman发送POST请求,地址:
http://localhost:8080/oauth/token,参数如下:-
请求头:添加**
Authorization: Basic Y2xpZW50MToxMjM0NTY=**(client1:123456的Base64编码); -
请求体(form-data):
grant_type=authorization_code、code=步骤2获取的授权码、redirect_uri=http://localhost:8081/callback、scope=all; -
响应结果:会返回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核心流程)
-
获取授权码:访问**
http://localhost:8080/oauth/authorize?client_id=client1&response_type=code&redirect_uri=http://localhost:8081/callback&scope=all**,登录admin用户(密码123456),获取回调地址中的code; -
获取access_token:通过Postman发送POST请求到
http://localhost:8080/oauth/token,携带code、client_id、client_secret等参数,获取access_token(JWT令牌); -
访问资源服务器接口:
-
访问普通用户接口:
http://localhost:8081/api/user/test,请求头添加Authorization: Bearer 第一步获取的access_token,可正常访问; -
访问管理员接口:
http://localhost:8081/api/admin/test,请求头添加相同的access_token,可正常访问(admin角色拥有权限);
-
-
测试单点登录:新增另一个资源服务器(如订单服务,端口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配置抽取为公共依赖,所有微服务引入,减少重复开发;
-
日志与监控:添加令牌生成、验证、失效的日志记录,监控令牌使用情况,及时发现异常访问。
八、总结
-
授权服务器:独立部署,负责用户认证、颁发授权码和JWT令牌,统一管理客户端和用户信息;
-
资源服务器:各个微服务,配置令牌验证规则,实现权限管控,仅允许携带合法令牌的请求访问;
-
单点登录:用户通过授权服务器一次登录,获取JWT令牌,即可访问所有授权的资源服务器,无需重复登录。