跳到主要内容 Spring Security 核心原理与实战应用 | 极客日志
Java java
Spring Security 核心原理与实战应用 Spring Security 是用于身份验证、授权和抵御常见攻击的框架。文章涵盖快速入门、默认行为及自动配置,演示了基于内存和数据库的认证流程。内容包括自定义登录页、会话并发管理、记住我功能及基于角色和权限的授权策略。此外,详细阐述了前后端分离架构下的 JWT 实现方案,涉及 Token 生成、Redis 缓存及过滤器链配置,并简要介绍了 OAuth2 授权码模式及 GitHub 社交登录集成步骤。
萤火微光 发布于 2026/3/15 更新于 2026/4/18 2 浏览
创建 Maven 项目 security1
启动应用,会生成默认用户名 user 和默认随机密码(打印在了控制台)
测试
Postman 访问 http://localhost:8080/hello,返回状态码 401 Unauthorized 和响应头 WWW-Authenticate: Basic realm="xxx",即需要进行 Basic 认证
Postman 使用 Http Basic 认证方式,在请求头中携带用户名和密码信息,成功响应 hello world
Http Basic:Postman 将用户名和密码组合成 username:password 的格式,然后使用 Base64 编码后放入请求头的 Authorization 字段中
浏览器访问 http://localhost:8080/hello,会重定向到默认生成的登录页面 http://localhost:8080/login
输入用户名密码登录,成功响应 hello world
创建配置文件 application.yml,指定用户名和密码
spring:
security:
user:
name: admin
password: 123456
package cn.freyfang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Main {
public static void main (String[] args) {
SpringApplication.run(Main.class, args);
}
}
package cn.freyfang.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/hello")
public String hello () {
return "hello world" ;
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<project xmlns ="http://maven.apache.org/POM/4.0.0"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
<modelVersion > 4.0.0</modelVersion >
<groupId > cn.freyfang</groupId >
<artifactId > security1</artifactId >
<version > 1.0-SNAPSHOT</version >
<properties >
<maven.compiler.source > 24</maven.compiler.source >
<maven.compiler.target > 24</maven.compiler.target >
<project.build.sourceEncoding > UTF-8</project.build.sourceEncoding >
</properties >
<parent >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-parent</artifactId >
<version > 3.5.9</version >
</parent >
<dependencies >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-web</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-test</artifactId >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-security</artifactId >
</dependency >
</dependencies >
<build >
<plugins >
<plugin >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-maven-plugin</artifactId >
</plugin >
</plugins >
</build >
</project >
2、默认行为
访问任何端点都需要经过身份验证
在启动时生成一个默认用户及随机密码
采用 BCrypt 等算法对密码存储进行保护
生成默认的登录和注销页面
提供基于表单的登录和注销流程
对基于表单的登录以及 HTTP Basic 进行验证
提供内容协商功能:对于 web 请求,会重定向到登录页面;对于服务请求,则返回 401 未授权
处理跨站请求伪造(CSRF)攻击
处理会话劫持攻击
写入 Strict-Transport-Security 以确保 HTTPS
写入 X-Content-Type-Options 以处理嗅探攻击
写入 Cache Control 头来保护经过身份验证的资源
写入 X-Frame-Options 以处理点击劫持攻击
发布认证成功和失败事件
3、自动配置 UserDetailsServiceAutoConfiguration 注入默认的 UserDetailsService,即基于内存的 InMemoryUserDetailsManager
@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@Conditional(MissingAlternativeOrUserPropertiesConfigured.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class}, type = "org.springframework.security.oauth2.jwt.JwtDecoder")
@ConditionalOnWebApplication(type = Type.SERVLET)
public class UserDetailsServiceAutoConfiguration {
private static final String NOOP_PASSWORD_PREFIX = "{noop}" ;
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$" );
private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager (SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager (User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build());
}
private String getOrDeducePassword (SecurityProperties.User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.warn(String.format("%n%nUsing generated security password: %s%n%nThis generated password is for development use only. " + "Your security configuration must be updated before running your application in " + "production.%n" , user.getPassword()));
}
if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
return password;
}
return NOOP_PASSWORD_PREFIX + password;
}
}
SecurityProperties 中指定了默认用户名和默认密码
@ConfigurationProperties("spring.security")
public class SecurityProperties {
public static final int BASIC_AUTH_ORDER = 2147483642 ;
public static final int DEFAULT_FILTER_ORDER = -100 ;
private final Filter filter = new Filter ();
private final User user = new User ();
public static class Filter {
}
public static class User {
private String name = "user" ;
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList <>();
private boolean passwordGenerated = true ;
}
}
SpringBootWebSecurityConfiguration 注入了默认的 SecurityFilterChain
@Bean
@Order(2147483642)
SecurityFilterChain defaultSecurityFilterChain (HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> ((AuthorizeHttpRequestsConfigurer.AuthorizedUrl) requests.anyRequest()).authenticated());
http.formLogin(Customizer.withDefaults());
http.httpBasic(Customizer.withDefaults());
return (SecurityFilterChain) http.build();
}
spring-boot-autoconfigure-3.5.9.jar!\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中配置了 SecurityAutoConfiguration 自动配置类
@AutoConfiguration(before = {UserDetailsServiceAutoConfiguration.class})
@ConditionalOnClass({DefaultAuthenticationEventPublisher.class})
@EnableConfigurationProperties({SecurityProperties.class})
public class SecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean({AuthenticationEventPublisher.class})
public DefaultAuthenticationEventPublisher authenticationEventPublisher (ApplicationEventPublisher publisher) {
return new DefaultAuthenticationEventPublisher (publisher);
}
}
4、核心组件 是 SecurityFilterChain 的实现类,程序启动后,查看默认加载了以下 16 个过滤器
DisableEncodeUrlFilter WebAsyncManagerIntegrationFilter SecurityContextHolderFilter HeaderWriterFilter CsrfFilter 处理 Csrf LogoutFilter 注销 UsernamePasswordAuthenticationFilter 认证 DefaultResourcesFilter 生成默认 css 样式 DefaultLoginPageGeneratingFilter 生成默认登录页 DefaultLogoutPageGeneratingFilter 生成默认登出页 BasicAuthenticationFilter 处理 Http Basic 请求认证 RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter ExceptionTranslationFilter 异常处理 AuthorizationFilter 授权
二、身份认证
1、基于内存的认证 创建配置类 SecurityConfig,注入 InMemoryUserDetailsManager
package cn.freyfang.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService () {
User.UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users.username("user" ).password("111111" ).roles("USER" ).build();
UserDetails admin = users.username("admin" ).password("222222" ).roles("USER" , "ADMIN" ).build();
return new InMemoryUserDetailsManager (user, admin);
}
}
2、认证流程
用户输入账号密码,点击登录
如果请求地址是 /login 且请求方式是 Post,则被 UsernamePasswordAuthenticationFilter 的 doFilter() 方法处理
将用户输入的账号密码封装成 UsernamePasswordAuthenticationToken 对象
调用认证管理器的实现类 ProviderManager 的 Authentication authenticate(Authentication authentication) 方法认证
如果认证成功,将 Authentication 用户信息存入 session 和 SecurityContext,并调用 AuthenticationSuccessHandler
如果认证失败,清空 SecurityContext 中,并调用 AuthenticationFailureHandler
ProviderManager 实际调用 DaoAuthenticationProvider 进行认证
调用 UserDetailsService 某个实现类的 UserDetails loadUserByUsername(String username) 方法从内存或 DB 中加载用户信息
调用 additionalAuthenticationChecks(userDetails, (UsernamePasswordAuthenticationToken) authentication) 方法校验密码,通过 PasswordEncoder 比较加载出来的用户密码和用户提交的密码是否一致
如果认证成功,将用户名密码和加载出来的权限信息封装为 UsernamePasswordAuthenticationToken 对象
3、基于 DB 的认证
创建数据库 security 及用户表 user
启动应用,访问 http://localhost:8080/hello,输入数据库中的用户名和密码进行认证
package cn.freyfang;
import cn.freyfang.mapper.UserMapper;
import cn.freyfang.model.SysUser;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
public class TestUser {
@Autowired
private UserMapper userMapper;
@Autowired
private PasswordEncoder passwordEncoder;
@Test
public void addUser () {
SysUser user = new SysUser ();
user.setUsername("admin" );
user.setPassword(passwordEncoder.encode("123456" ));
System.out.println("user = " + user);
int insert = userMapper.insert(user);
System.out.println("insert = " + insert);
}
}
自定义 UserDetailsService 的实现类 UserDetailsServiceImpl,并将实例注入到容器中
package cn.freyfang.service;
import cn.freyfang.mapper.UserMapper;
import cn.freyfang.model.SysUser;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException {
SysUser user = userMapper.selectOne(Wrappers.lambdaQuery(SysUser.class).eq(SysUser::getUsername, username));
if (user == null ) {
throw new UsernameNotFoundException ("user '" + username + "' not found" );
}
Set<String> permissions = userService.selectPermissions(user);
Collection<GrantedAuthority> authorities = new ArrayList <>();
for (String permission : permissions) {
authorities.add(() -> permission);
}
return new User (user.getUsername(), user.getPassword(), true , true , true , true , authorities);
}
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder ();
}
创建实体类 SysUser、UserMapper、UserService
@Data
@TableName("user")
public class SysUser {
private Long id;
private String username;
private String password;
}
@Mapper
public interface UserMapper extends BaseMapper <SysUser> {}
@Service
public class UserService {
public Set<String> selectPermissions (SysUser user) {
return Set.of("index:hello" , "ROLE_admin" );
}
}
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
org.springframework.security: DEBUG
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
</dependency >
<dependency >
<groupId > com.baomidou</groupId >
<artifactId > mybatis-plus-spring-boot3-starter</artifactId >
<version > 3.5.11</version >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
<version > 8.0.23</version >
</dependency >
4、常用配置
自定义登录页,开启后会自动禁用以下过滤器:DefaultResourcesFilter、DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter、BasicAuthenticationFilter
自定义登录请求参数名
自定义登录成功/失败处理器、自定义退出成功处理器
获取用户信息
会话并发管理,开启后会自动启用以下过滤器:ConcurrentSessionFilter、SessionManagementFilter
测试
访问 http://localhost:8080/,会重定向到 /to-login,响应自定义登录页面
输入错误的用户名或密码,会转发到 /login-fail,响应 loginFail
输入正确的用户名或密码,会转发到 /,响应自定义首页
使用另一个浏览器登录相同的账号,然后当前浏览器访问 /hello,响应 {"msg":"该账号已从其他设备登录","code":500}
修改配置类,自定义 SecurityFilterChain 实例
@Bean
SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception {
http.authorizeHttpRequests(requests -> requests
.requestMatchers("/to-login" , "/login-fail" ).permitAll()
.anyRequest().authenticated()
);
http.formLogin(form -> form
.loginPage("/to-login" )
.usernameParameter("name" )
.passwordParameter("pwd" )
.loginProcessingUrl("/login" )
.successForwardUrl("/" )
.failureForwardUrl("/login-fail" )
);
http.logout(logout -> logout
.logoutUrl("/logout" )
.logoutSuccessUrl("/to-login" )
);
http.sessionManagement(session -> {
session.maximumSessions(1 )
.expiredSessionStrategy(event -> {
System.out.println("=====================会话并发" );
String result = new ObjectMapper ().writeValueAsString(Map.of("code" , 500 , "msg" , "该账号已从其他设备登录" ));
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8" );
response.getWriter().write(result);
});
});
return http.build();
}
package cn.freyfang.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class IndexController {
@GetMapping("/hello")
@ResponseBody
public String hello () {
return "hello world" ;
}
@GetMapping("/to-login")
public String toLogin () {
return "login" ;
}
@RequestMapping("/")
public String index (Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
model.addAttribute("username" , username);
return "index" ;
}
@PostMapping("/login-fail")
@ResponseBody
public String loginFail () {
return "loginFail" ;
}
}
创建首页 src/main/resources/templates/index.html
<!DOCTYPE html >
<html lang ="en" xmlns:th ="https://www.thymeleaf.org" >
<head >
<meta charset ="UTF-8" >
<title > Title</title >
</head >
<body >
<h1 > 首页</h1 >
<div th:text ="'欢迎:'+${username}" > </div >
<form th:action ="@{/logout}" method ="post" >
<input type ="submit" value ="退出" > </input >
</form >
<a th:href ="@{/hello}" > hello</a >
</body >
</html >
创建登录页面 src/main/resources/templates/login.html
<!DOCTYPE html >
<html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="https://www.thymeleaf.org" >
<head >
<title > Please Log In</title >
</head >
<body >
<h1 > Please Log In</h1 >
<div th:if ="${param.error}" > Invalid username and password. </div >
<div th:if ="${param.logout}" > You have been logged out. </div >
<form th:action ="@{/login}" method ="post" >
<div > <input type ="text" name ="name" placeholder ="Username" /> </div >
<div > <input type ="password" name ="pwd" placeholder ="Password" /> </div >
<input type ="submit" value ="Log in" /> </form >
</body >
</html >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-thymeleaf</artifactId >
</dependency >
5、记住我
两种实现方式
使用 TokenBasedRememberMeServices,即基于 cookie 保存 token。如果 Cookie 被窃取,攻击者可以在有效期内伪造身份。后台无法主动使令牌失效(除非修改密码或等待过期)
使用 PersistentTokenBasedRememberMeServices(推荐),即使用数据库持久化 token。服务端维护令牌的状态,支持主动撤销令牌或删除记录。每次成功验证后,旧令牌会被替换为新令牌,防止令牌被重复使用
1)步骤
访问登录页面,勾选 记住我 复选框,登录成功后,会自动向 cookies 和 persistent_logins 表中写入 token
关闭浏览器后重新打开访问,会自动进行认证,无需再输入用户名密码
@Bean
SecurityFilterChain securityFilterChain (HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
http.rememberMe(rememberMe -> rememberMe
.rememberMeServices(rememberMeServices)
return http.build();
}
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Bean
public RememberMeServices rememberMeServices (PersistentTokenRepository tokenRepository) {
PersistentTokenBasedRememberMeServices services = new PersistentTokenBasedRememberMeServices ("123456" , userDetailsService, tokenRepository);
services.setTokenValiditySeconds(60 * 60 * 24 * 7 );
services.setParameter("remember" );
return services;
}
@Bean
public PersistentTokenRepository persistentTokenRepository () {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl ();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
<div > 记住我:<input type ="checkbox" name ="remember" /> </div >
create table persistent_logins (
username varchar (64 ) not null ,
series varchar (64 ) primary key ,
token varchar (64 ) not null ,
last_used timestamp not null
)
2)流程
设置 rememberMe 配置后会启用 RememberMeAuthenticationFilter 过滤器拦截请求
如果 SecurityContextHolder 中没有已认证用户信息,则执行自动登录
从请求头携带的 cookie 中解析 remember-me,并解码出 series 和 token
然后根据 series 从数据库中查找并返回 PersistentRememberMeToken。如果找不到则异常
比较 cookie 中的 token 和数据库中的 token 是否一致,如果不一致则异常,可能 cookie 已被窃取
判断 token 是否过期:数据库中的 last_used + token 有效时间,如果小于当前时间则异常
生成新的 token,更新到 cookie 和数据库
使用 UserDetailsService 查询用户信息和权限,封装成 RememberMeAuthenticationToken
通过 ProviderManager 认证管理器走认证流程
三、授权
基于角色或权限进行访问控制
常用方法
hasAuthority(String authority) 表示访问资源需要 authority 权限
hasAnyAuthority(String... authorities) 需要 authorities 中的任一权限
hasRole(String role) 需要 role 角色
hasAnyRole(String... roles) 需要 roles 中的任一角色
1、步骤
1)创建资源 修改 IndexController,新增 3 个方法
@GetMapping("/hey")
@ResponseBody
public String hey () {
return "hey" ;
}
@GetMapping("/hi")
@ResponseBody
public String hi () {
return "hi" ;
}
@GetMapping("/ok")
@ResponseBody
public String ok () {
return "ok" ;
}
2)为资源设置所需权限
方式 2:基于 Controller 方法
在配置类上使用 @EnableMethodSecurity 开启方法授权
常用注解
@PreAuthorize 适合进入方法前的权限验证
@PostAuthorize 在方法执行后再进行权限验证,适合验证带有返回值的权限
@PreFilter 进入方法之前对数据进行过滤
@PostFilter 权限验证之后对数据进行过滤
修改 IndexController,添加鉴权注解。同时删除方式 1 的权限配置
@PreAuthorize("hasAuthority('index:hey')")
@GetMapping("/hey")
@ResponseBody
public String hey () {
return "hey" ;
}
@PreAuthorize("hasAuthority('index:hi')")
@GetMapping("/hi")
@ResponseBody
public String hi () {
return "hi" ;
}
@PreAuthorize("hasRole('admin')")
@GetMapping("/ok")
@ResponseBody
public String ok () {
return "ok" ;
}
@Bean
SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception {
http.authorizeHttpRequests(requests -> requests
.requestMatchers("/to-login" , "/login-fail" ).permitAll()
.requestMatchers("/hey" ).hasAuthority("index:hey" )
.requestMatchers("/hi" ).hasAuthority("index:hi" )
.requestMatchers("/ok" ).hasRole("admin" )
.anyRequest().authenticated()
);
return http.build();
}
3)为用户授权 用户登录时,从数据库中查询权限信息保存到 Authentication 对象中。修改 UserService,假如用户具有 "index:hey" 和 "ROLE_admin" 权限
@Service
public class UserService {
public Set<String> selectPermissions (SysUser user) {
return Set.of("index:hey" , "ROLE_admin" );
}
}
4)请求未授权的接口
访问 /hey、/ok 正常响应
访问 /hi,响应默认 403 Forbidden 页面
2、自定义异常处理
@GetMapping("/unauthorized")
@ResponseBody
public String unauthorized () {
return "未授权" ;
}
修改配置类的 SecurityFilterChain 实例
http.exceptionHandling(exception -> exception
.accessDeniedPage("/unauthorized" )
3、授权流程
授权过滤器 AuthorizationFilter 负责拦截用户请求,先调用授权管理器 RequestMatcherDelegatingAuthorizationManager 处理,它将不同请求委托给特定的授权管理器
如果访问无需认证的资源(如 /to-login),则由 SingleResultAuthorizationManager 处理,总是允许
如果访问只需认证就能访问的资源(如 /、/hello),则由 AuthenticatedAuthorizationManager 处理,判断当前用户是否已经过认证
如果访问需要授权才能访问的资源(如 /hey),则由 AuthorityAuthorizationManager 处理,检查当前已认证身份信息中是否包含所需的权限
如果使用基于方法的授权,即开启了 @EnableMethodSecurity,则会 import 一个方法拦截器 AuthorizationManagerBeforeMethodInterceptor
先由 AuthorizationFilter 调用 AuthenticatedAuthorizationManager,判断当前用户是否已经过认证
再由 AuthorizationManagerBeforeMethodInterceptor 调用 PreAuthorizeAuthorizationManager,检查当前已认证身份信息中是否包含目标方法上 @PreAuthorize 注解中要求的权限
四、前后端分离
1、JWT
JWT(JSON Web Token)是一种开放标准,用于在各方之间安全地传输信息。它主要用于身份验证和信息交换
默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段
JWT 由三部分组成,用点(.)分隔
Header(头部):包含令牌类型和签名算法,使用 Base64Url 编码
Payload(载荷):包含声明信息(用户信息、权限等),使用 Base64Url 编码
Signature(签名):用于验证令牌完整性,使用加密签名算法
如 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
工作原理:用户登录 → 服务器生成 JWT → 客户端存储 → 后续请求携带 JWT → 服务器验证
JWT 特别适合 RESTful API 和微服务架构的身份认证场景
2、搭建基础环境 package cn.freyfang.service;
import cn.freyfang.model.SysUser;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class UserService {
public SysUser getUserByName (String username) {
return new SysUser (1L , "admin" , "$2a$10$79iMOUW67OL6s1SaAsYgp.67.ivR34LT6h80uJNUvxEnnrgKhqVJq" );
}
public Set<String> getPermissionsByUser (SysUser user) {
return Set.of("index:hello" , "ROLE_admin" );
}
public Set<String> getRolesByUser (SysUser user) {
return Set.of("ROLE_admin" );
}
}
package cn.freyfang.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SysUser implements Serializable {
private static final long serialVersionUID = 1L ;
private Long userId;
private String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
}
创建全局异常处理器 GlobalExceptionHandler
package cn.freyfang.exception;
import cn.freyfang.model.R;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public R handleRuntimeException (RuntimeException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址 '{}' ,发生未知异常。" , requestURI, e);
return R.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public R handleException (Exception e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址 '{}' ,发生系统异常。" , requestURI, e);
return R.error(e.getMessage());
}
}
package cn.freyfang.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class R {
private Integer code;
private String msg;
private Object data;
public static R ok () {
return ok("操作成功" );
}
public static R ok (String msg) {
return ok(msg, null );
}
public static R ok (Object data) {
return ok("操作成功" , data);
}
public static R ok (String msg, Object data) {
return new R (200 , msg, data);
}
public static R error () {
return error("操作失败" );
}
public static R error (String msg) {
return error(msg, null );
}
public static R error (String msg, Object data) {
return new R (500 , msg, data);
}
public static R error (Integer code, String msg) {
return new R (code, msg, null );
}
}
package cn.freyfang.util;
import cn.freyfang.contstant.Constants;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import java.io.IOException;
public class ResponseUtil {
public static void out (HttpServletResponse response, String string) {
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(Constants.UTF8);
try {
response.getWriter().print(string);
} catch (IOException e) {
throw new RuntimeException (e);
}
}
}
package cn.freyfang.config;
import cn.freyfang.contstant.Constants;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.nio.charset.Charset;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate <>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer (Object.class);
template.setKeySerializer(new StringRedisSerializer ());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer ());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
private class FastJson2JsonRedisSerializer <T> implements RedisSerializer <T> {
public static final Charset DEFAULT_CHARSET = Charset.forName(Constants.UTF8);
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
private Class<T> clazz;
public FastJson2JsonRedisSerializer (Class<T> clazz) {
super ();
this .clazz = clazz;
}
@Override
public byte [] serialize(T t) throws SerializationException {
if (t == null ) {
return new byte [0 ];
}
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize (byte [] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0 ) {
return null ;
}
String str = new String (bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
}
}
}
package cn.freyfang.contstant;
import io.jsonwebtoken.Claims;
public class Constants {
public static final String UTF8 = "UTF-8" ;
public static final String TOKEN = "token" ;
public static final String TOKEN_PREFIX = "Bearer " ;
public static final String LOGIN_USER_KEY = "login_user_key" ;
public static final String LOGIN_TOKEN_KEY = "login_tokens:" ;
public static final String JWT_USERNAME = Claims.SUBJECT;
public static final String[] JSON_WHITELIST_STR = {"cn.freyfang" };
}
<?xml version="1.0" encoding="UTF-8" ?>
<project xmlns ="http://maven.apache.org/POM/4.0.0"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
<modelVersion > 4.0.0</modelVersion >
<groupId > cn.freyfang</groupId >
<artifactId > security2</artifactId >
<version > 1.0-SNAPSHOT</version >
<properties >
<maven.compiler.source > 24</maven.compiler.source >
<maven.compiler.target > 24</maven.compiler.target >
<project.build.sourceEncoding > UTF-8</project.build.sourceEncoding >
</properties >
<parent >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-parent</artifactId >
<version > 3.5.9</version >
</parent >
<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 > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-test</artifactId >
</dependency >
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
</dependency >
<dependency >
<groupId > io.jsonwebtoken</groupId >
<artifactId > jjwt</artifactId >
<version > 0.9.1</version >
</dependency >
<dependency >
<groupId > javax.xml.bind</groupId >
<artifactId > jaxb-api</artifactId >
<version > 2.3.1</version >
</dependency >
<dependency >
<groupId > com.alibaba.fastjson2</groupId >
<artifactId > fastjson2</artifactId >
<version > 2.0.60</version >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-data-redis</artifactId >
</dependency >
</dependencies >
<build >
<plugins >
<plugin >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-maven-plugin</artifactId >
</plugin >
</plugins >
</build >
</project >
3、创建核心配置 package cn.freyfang.config;
import cn.freyfang.filter.JwtAuthenticationTokenFilter;
import cn.freyfang.model.LoginUser;
import cn.freyfang.model.R;
import cn.freyfang.util.ResponseUtil;
import cn.freyfang.util.TokenUtil;
import com.alibaba.fastjson2.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private TokenUtil tokenUtil;
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder ();
}
@Bean
public AuthenticationManager authenticationManager (AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(request -> request
.requestMatchers("/login" ).permitAll()
.requestMatchers(HttpMethod.GET, "/" , "/*.html" , "/**.html" , "/**.css" , "/**.js" ).permitAll()
.anyRequest().authenticated()
);
http.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
String msg = String.format("请求访问:%s,认证失败,无法访问系统资源" , request.getRequestURI());
ResponseUtil.out(response, JSON.toJSONString(R.error(401 , msg)));
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
String msg = String.format("请求访问:%s,授权失败,无法访问系统资源" , request.getRequestURI());
ResponseUtil.out(response, JSON.toJSONString(R.error(403 , msg)));
})
);
http.logout(logout -> logout
.logoutUrl("/logout" )
.logoutSuccessHandler((request, response, authentication) -> {
LoginUser loginUser = tokenUtil.getLoginUser(request);
if (loginUser != null ) {
tokenUtil.delLoginUser(loginUser.getToken());
}
ResponseUtil.out(response, JSON.toJSONString(R.ok("退出成功" )));
})
);
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
package cn.freyfang.controller;
import cn.freyfang.contstant.Constants;
import cn.freyfang.model.LoginUser;
import cn.freyfang.model.R;
import cn.freyfang.model.SysUser;
import cn.freyfang.service.UserService;
import cn.freyfang.util.TokenUtil;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.Set;
@RestController
public class UserController {
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private TokenUtil tokenUtil;
@Autowired
private UserService userService;
@PostMapping("/login")
public R login (@RequestBody SysUser loginBody) {
String username = loginBody.getUsername();
String password = loginBody.getPassword();
Authentication authentication = null ;
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (username, password);
authentication = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
throw new RuntimeException ("用户名或密码错误" );
} else {
throw new RuntimeException (e.getMessage());
}
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String token = tokenUtil.createToken(loginUser);
return R.ok(Map.of(Constants.TOKEN, token));
}
@GetMapping("getInfo")
public R getInfo () {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SysUser user = loginUser.getUser();
Set<String> roles = userService.getRolesByUser(user);
Set<String> permissions = userService.getPermissionsByUser(user);
if (!loginUser.getPermissions().equals(permissions)) {
loginUser.setPermissions(permissions);
tokenUtil.refreshToken(loginUser);
}
return R.ok(Map.of("user" , user, "roles" , roles, "permissions" , permissions));
}
}
package cn.freyfang.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/")
public String index () {
return "欢迎" ;
}
@PreAuthorize("hasAuthority('index:hello')")
@GetMapping("/hello")
public String hello () {
return "hello" ;
}
@PreAuthorize("hasAuthority('index:hi')")
@GetMapping("/hi")
public String hi () {
return "hi" ;
}
@PreAuthorize("hasRole('admin')")
@GetMapping("/ok")
public String ok () {
return "ok" ;
}
}
创建 JwtAuthenticationTokenFilter
package cn.freyfang.filter;
import cn.freyfang.model.LoginUser;
import cn.freyfang.util.TokenUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenUtil tokenUtil;
@Override
protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
LoginUser loginUser = tokenUtil.getLoginUser(request);
if (loginUser != null && SecurityContextHolder.getContext().getAuthentication() == null ) {
tokenUtil.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (loginUser, null , loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource ().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
package cn.freyfang.util;
import cn.freyfang.contstant.Constants;
import cn.freyfang.model.LoginUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class TokenUtil {
@Value("${token.header:Authorization}")
private String header;
@Value("${token.secret:123456}")
private String secret;
@Value("${token.expireTime:30}")
private int expireTime;
private static final Long MILLIS_MINUTE_TWENTY = 20 * 60 * 1000L ;
@Autowired
public RedisTemplate redisTemplate;
public LoginUser getLoginUser (HttpServletRequest request) {
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "" );
}
if (StringUtils.isNotEmpty(token)) {
try {
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = (LoginUser) redisTemplate.opsForValue().get(userKey);
return user;
} catch (Exception e) {
log.error("获取用户信息异常 '{}'" , e.getMessage());
}
}
return null ;
}
public void delLoginUser (String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenKey(token);
redisTemplate.delete(userKey);
}
}
public String createToken (LoginUser loginUser) {
String token = UUID.randomUUID().toString();
loginUser.setToken(token);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap <>();
claims.put(Constants.LOGIN_USER_KEY, token);
claims.put(Constants.JWT_USERNAME, loginUser.getUsername());
return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
}
public void verifyToken (LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TWENTY) {
refreshToken(loginUser);
}
}
public void refreshToken (LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * 60 * 1000L );
String userKey = getTokenKey(loginUser.getToken());
redisTemplate.opsForValue().set(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
private String getTokenKey (String uuid) {
return Constants.LOGIN_TOKEN_KEY + uuid;
}
}
创建 UserDetailsService 实现类 UserDetailsServiceImpl
package cn.freyfang.service;
import cn.freyfang.model.LoginUser;
import cn.freyfang.model.SysUser;
import lombok.extern.slf4j.Slf4j;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Set;
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException {
SysUser user = userService.getUserByName(username);
if (user == null ) {
log.info("登录用户:{} 不存在。" , username);
throw new RuntimeException ("用户名或密码错误" );
}
Set<String> permissions = userService.getPermissionsByUser(user);
return new LoginUser (user.getUserId(), user, permissions);
}
}
创建 UserDetails 实现类 LoginUser
package cn.freyfang.model;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L ;
private Long userId;
private Set<String> permissions;
private SysUser user;
private String token;
private Long loginTime;
private Long expireTime;
public LoginUser (Long userId, SysUser user, Set<String> permissions) {
this .userId = userId;
this .user = user;
this .permissions = permissions;
}
@JSONField(serialize = false)
@Override
public String getPassword () {
return user.getPassword();
}
@Override
public String getUsername () {
return user.getUsername();
}
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired () {
return true ;
}
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked () {
return true ;
}
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired () {
return true ;
}
@JSONField(serialize = false)
@Override
public boolean isEnabled () {
return true ;
}
@JSONField(serialize = false)
@Override
public Collection<? extends GrantedAuthority > getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList <>();
for (String permission : permissions) {
if (StringUtils.hasLength(permission)) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority (permission);
authorities.add(authority);
}
}
return authorities;
}
}
4、测试
禁用了 csrf 相关过滤器:CsrfFilter
没有启用 formLogin 相关过滤器:UsernamePasswordAuthenticationFilter、DefaultResourcesFilter、DefaultLoginPageGeneratingFilter、DefaultLogoutPageGeneratingFilter
没有启用 httpBasic 相关过滤器:BasicAuthenticationFilter
注册了自定义过滤器:JwtAuthenticationTokenFilter
使用 Postman 等工具测试
请求 /login(该接口无需认证即可访问),携带请求体参数 {"username":"admin","password":"123456"},正确返回 JWT
先使用 AuthenticationManager 进行认证
认证成功生成 JWT,并将用户信息写入 redis。注意:JWT 中保存了 redis 中的 key
以下请求都要携带请求头 Authorization=Bearer ${token}
请求 /getInfo(该接口只需认证成功),正确响应用户信息
JwtAuthenticationTokenFilter 先进行处理:从 header 中解析 JWT,然后从 readis 中取出用户信息,并放入 SecurityContextHolder
AuthorizationFilter 再进行处理:如果 SecurityContextHolder 中有已认证的用户信息,则通过
AuthorizationManagerBeforeMethodInterceptor 再判断已认证的用户信息中是否具有访问目标方法的权限
请求 /hello(该接口需要授权,且用户有权限):正确响应
请求 /hi(该接口需要授权,且用户无权限):抛出 AuthorizationDeniedException
启动应用,查看 DefaultSecurityFilterChain 发现共启用了 11 个过滤器
DisableEncodeUrlFilter WebAsyncManagerIntegrationFilter SecurityContextHolderFilter HeaderWriterFilter LogoutFilter JwtAuthenticationTokenFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter ExceptionTranslationFilter AuthorizationFilter
五、OAuth2
OAuth 2.0 是一种开放授权协议,核心目标是让第三方应用'安全地获取'用户在某个服务商(如微信、GitHub、Google)上的有限权限,而无需用户将账号密码直接告诉第三方应用。主要用于社交登录
相关角色
资源所有者(Resource Owner):指用户
客户应用(Client):这里指我们自己创建的应用,即第三方应用
资源服务器(Resource Server):某个服务商,如 GitHub
授权服务器(Authorization Server):某个服务商,如 GitHub
四种模式
授权码(authorization-code):安全,且支持刷新令牌
隐藏式(implicit):适合纯前端应用,授权服务器直接将令牌通过 URL 片段返回给浏览器。极不安全,且不支持刷新令牌,过期后需要重新授权
密码式(password):适合高度信任的应用间授权。用户将自己的账号密码直接交给客户应用,客户应用再去授权服务器换令牌
客户端凭证(client credentials):适合服务间授权。该模式与用户无关,是客户应用需要访问自己创建的资源
1、授权码模式
最常用、最安全、功能最完整的模式,也是 OAuth 2.0 官方推荐的流程
通过一个临时的、一次性的'授权码'来交换最终的访问令牌,从而避免了令牌在浏览器中暴露的风险
流程步骤
客户应用如需开通某个社交登录功能,需要先在服务商网站上创建应用并获得 client_id 和 client_secret
用户访问客户应用的登录页面,选择社交登录方式 (GitHub/QQ 等)
客户应用重定向到授权服务器的授权端点,并携带自己的 client_id、请求的权限范围 scope、一个随机生成的 state(用于防止 CSRF 攻击)和一个回调地址 redirect_uri
用户登录与同意:用户在授权服务器的页面上登录,并确认是否同意授予客户应用所请求的权限
发放授权码:如果用户同意,授权服务器会将用户重定向回在之前提供的 redirect_uri,并在 URL 参数中附上一个授权码
交换访问令牌:客户应用的后端服务拿着这个授权码,加上自己的 client_id、client_secret(只有自己和授权服务器知道),向授权服务器的令牌端点发起请求,换取访问令牌和可选的刷新令牌
使用访问令牌:客户应用的后端服务拿到访问令牌后,就可以用它来调用资源服务器的 API,获取用户数据了
2、GitHub 社交登录
Spring Security 提供了对 OAuth2 客户端的原生支持,可以非常便捷地集成 GitHub 登录功能。其核心原理是:Spring Security 作为 OAuth2 客户端,而 GitHub 作为 OAuth2 授权服务器和资源服务器
登录 GitHub,在 Settings-> Developer Settings-> OAuth Apps 页面创建应用,设置回调地址 http://localhost:8080/login/oauth2/code/github,获取 Client ID、Client secrets
创建 Maven 项目 security3
启动应用,访问自动生成的登录页面 http://localhost:8080/login,点击页面上的 GitHub 链接 http://localhost:8080/oauth2/authorization/github
该接口会拼接参数让浏览器 重定向到 GitHub 授权页面:https://github.com/login/oauth/authorize?response_type=code&client_id=xxx&scope=read:user&state=3BNHSDAD2i4zoqOjBQ8Bydps69wnXq6CU3gnqw5xum4%3D&redirect_uri=http://localhost:8080/login/oauth2/code/github
GitHub 检测当前用户未登录,重定向到 GitHub 登录页面 https://github.com/login?client_id=&return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3Dxxx%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8080%252Flogin%252Foauth2%252Fcode%252Fgithub%26response_type%3Dcode%26scope%3Dread%253Auser%26state%3D3BNHSDAD2i4zoqOjBQ8Bydps69wnXq6CU3gnqw5xum4%253D,输入 GitHub 账号密码并登录
浏览器重定向到授权页面:https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin%2Foauth2%2Fcode%2Fgithub&response_type=code&scope=read%3Auser&state=3BNHSDAD2i4zoqOjBQ8Bydps69wnXq6CU3gnqw5xum4%3D,点击授权
GitHub 携带授权码回调后端服务 http://localhost:8080/login/oauth2/code/github?code=xxx&state=3BNHSDAD2i4zoqOjBQ8Bydps69wnXq6CU3gnqw5xum4%3D
后端服务访问 https://github.com/login/oauth/access_token 获取 access_token,携带参数 {grant_type=[authorization_code], code=[xxx], redirect_uri=[http://localhost:8080/login/oauth2/code/github]}
后端服务访问 https://api.github.com/user 获取用户信息,携带 access_token
创建 IndexController 接收返回信息
@RestController
public class IndexController {
@GetMapping("/")
public Map<String, Object> index (@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User oauth2User) {
String principalName = authorizedClient.getPrincipalName();
System.out.println("principalName = " + principalName);
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
System.out.println("clientRegistration.getClientName() = " + clientRegistration.getClientName());
System.out.println("clientRegistration.getRedirectUri() = " + clientRegistration.getRedirectUri());
System.out.println("oauth2User.getName() = " + oauth2User.getName());
System.out.println("oauth2User.getAttributes() = " + oauth2User.getAttributes());
System.out.println("oauth2User.getAuthorities() = " + oauth2User.getAuthorities());
return Map.of("OAuth2AuthorizedClient" , authorizedClient, "OAuth2User" , oauth2User);
}
}
spring:
security:
oauth2:
client:
registration:
github:
client-id: xxx
client-secret: xxx
CommonOAuth2Provider 中预定义了一些服务商的属性
package org.springframework.security.config.oauth2.client;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
public enum CommonOAuth2Provider {
GITHUB {
public ClientRegistration.Builder getBuilder (String registrationId) {
ClientRegistration.Builder builder = this .getBuilder(registrationId, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}" );
builder.scope(new String []{"read:user" });
builder.authorizationUri("https://github.com/login/oauth/authorize" );
builder.tokenUri("https://github.com/login/oauth/access_token" );
builder.userInfoUri("https://api.github.com/user" );
builder.userNameAttributeName("id" );
builder.clientName("GitHub" );
return builder;
}
}
}
<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 > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-oauth2-client</artifactId >
</dependency >
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online