基于 Spring Boot 實(shí)現(xiàn)單點(diǎn)登錄:從原理到實(shí)踐
作者:一安
單點(diǎn)登錄(??Single Sign-On??,??SSO??)是企業(yè)級(jí)應(yīng)用架構(gòu)中至關(guān)重要的身份認(rèn)證方案,它允許用戶在一次登錄后,無(wú)需重復(fù)認(rèn)證即可訪問(wèn)多個(gè)相互信任的應(yīng)用系統(tǒng)。
引言
單點(diǎn)登錄(Single Sign-On,SSO)是企業(yè)級(jí)應(yīng)用架構(gòu)中至關(guān)重要的身份認(rèn)證方案,它允許用戶在一次登錄后,無(wú)需重復(fù)認(rèn)證即可訪問(wèn)多個(gè)相互信任的應(yīng)用系統(tǒng)。
原理剖析
圖片
- 用戶首次訪問(wèn):用戶訪問(wèn)應(yīng)用系統(tǒng)`1 的服務(wù)資源時(shí),因未登錄被重定向到認(rèn)證中心。
- 認(rèn)證中心處理:用戶在認(rèn)證中心完成賬號(hào)密碼驗(yàn)證后,認(rèn)證中心生成并返回
Token。 - 多系統(tǒng)信任:用戶攜帶
Token訪問(wèn)應(yīng)用系統(tǒng)1或應(yīng)用系統(tǒng)2時(shí),應(yīng)用系統(tǒng)會(huì)請(qǐng)求認(rèn)證中心驗(yàn)證Token的有效性,驗(yàn)證通過(guò)后即可獲取資源。
項(xiàng)目實(shí)戰(zhàn)
依賴模塊
// 通用依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>JWT工具類
// JWT工具類
public class JwtUtils {
private static final String SECRET = "12345678901234567890123456789012";
private static final long EXPIRATION = 3600000L; // 1小時(shí)
// 生成Token
public static String generateToken(String username) {
return Jwts.builder()
.subject(username)
.expiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(KeyUtil.hmacKey(SECRET))
.compact();
}
// 解析Token
public static Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(KeyUtil.hmacKey(SECRET))
.build()
.parseSignedClaims(token)
.getPayload();
}
// 驗(yàn)證Token是否過(guò)期
public static boolean isTokenExpired(String token) {
try {
Claims claims = parseToken(token);
return claims.getExpiration().before(new Date());
} catch (Exception e) {
returntrue;
}
}
}
// 輔助類:Key工具
class KeyUtil {
static SecretKey hmacKey(String secret) {
return Keys.hmacShaKeyFor(secret.getBytes());
}
}認(rèn)證中心
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/login")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/verify")).permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/do-login")
.successHandler((request, response, authentication) -> {
String token = JwtUtils.generateToken(authentication.getName());
// 重定向到原應(yīng)用并攜帶Token
String redirectUrl = request.getParameter("redirectUrl");
response.sendRedirect(redirectUrl + "?token=" + token);
})
.failureHandler((request, response, exception) -> {
String redirectUrl = request.getParameter("redirectUrl");
response.sendRedirect("/login?redirectUrl=" + redirectUrl + "&error=true");
})
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
if (!manager.userExists("admin")) {
manager.createUser(User.withUsername("admin")
.password(passwordEncoder().encode("123456"))
.roles("ADMIN")
.build());
}
return manager;
}
}@Controller
public class AuthController {
@GetMapping("/login")
public String loginPage() {
return"sso-login.html";
}
// Token驗(yàn)證接口
@PostMapping("/verify")
@ResponseBody
public ResponseEntity<Boolean> verifyToken(@RequestParam String token) {
try {
JwtUtils.parseToken(token);
return ResponseEntity.ok(!JwtUtils.isTokenExpired(token));
} catch (Exception e) {
return ResponseEntity.ok(false);
}
}
}應(yīng)用系統(tǒng)模塊(app1 和 app2)
@Configuration
@EnableWebSecurity
public class AppSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.addFilterBefore(new SsoFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 自定義Token驗(yàn)證過(guò)濾器
class SsoFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getParameter("token");
if (token == null) {
token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
}
}
if (token != null) {
try {
// 調(diào)用認(rèn)證中心的驗(yàn)證接口
RestTemplate restTemplate = new RestTemplate();
boolean isValid = restTemplate.postForObject("http://localhost:8080/verify?token=" + token, null, Boolean.class);
if (isValid) {
Claims claims = JwtUtils.parseToken(token);
String username = claims.getSubject();
// 構(gòu)建用戶認(rèn)證信息
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// Token無(wú)效,重定向到認(rèn)證中心
response.sendRedirect("http://localhost:8080/login?redirectUrl=" + request.getRequestURL());
return;
}
} else {
// 無(wú)Token,重定向到認(rèn)證中心
response.sendRedirect("http://localhost:8080/login?redirectUrl=" + request.getRequestURL());
return;
}
filterChain.doFilter(request, response);
}
}
}// 應(yīng)用系統(tǒng)的Controller
@RestController
public class AppController {
@GetMapping("/resource")
public String getResource(Authentication authentication) {
return "歡迎 " + authentication.getName() + " 訪問(wèn)應(yīng)用系統(tǒng)1的資源!";
}
}系統(tǒng)測(cè)試與驗(yàn)證

- 啟動(dòng)服務(wù):依次啟動(dòng)認(rèn)證中心(
8080)、應(yīng)用系統(tǒng) 1(8081)、應(yīng)用系統(tǒng) 2(8082)。 - 訪問(wèn)應(yīng)用 1 資源:瀏覽器輸入
http://localhost:8081/resource,會(huì)自動(dòng)跳轉(zhuǎn)到認(rèn)證中心的登錄頁(yè)面http://localhost:8080/login?redirectUrl=http://localhost:8081/resource。 - 用戶登錄:輸入用戶名
admin和密碼123456,登錄成功后會(huì)攜帶Token重定向回應(yīng)用 1,此時(shí)可看到資源內(nèi)容。 - 訪問(wèn)應(yīng)用 2 資源:瀏覽器輸入
http://localhost:8082/resource,因已攜帶有效Token,無(wú)需再次登錄即可直接訪問(wèn)資源。
優(yōu)化方向
Token存儲(chǔ)與黑名單機(jī)制:可將已注銷的Token存入Redis黑名單,避免Token被盜用。- 多端適配:針對(duì)移動(dòng)端(
APP、小程序),可優(yōu)化Token的存儲(chǔ)和傳遞方式(如存儲(chǔ)在Header中)。 - 高可用與集群:將認(rèn)證中心和應(yīng)用系統(tǒng)集群部署,配合
Nacos實(shí)現(xiàn)服務(wù)注冊(cè)與發(fā)現(xiàn)。 - 細(xì)粒度權(quán)限控制:結(jié)合
Spring Security的權(quán)限注解(如@PreAuthorize),實(shí)現(xiàn)接口級(jí)別的權(quán)限校驗(yàn)。
責(zé)任編輯:武曉燕
來(lái)源:
一安未來(lái)

































