## 一、SpringSecurity #### 1、简介 `Spring Security` 是一个高度可定制、功能强大且广泛使用的、为基于 Java 的企业应用提供认证、授权和其他安全功能的框架。 它的主要优点: - **功能全面**:覆盖了认证、授权、防护等方方面面。 - **高度可扩展**:几乎每一个组件都可以被定制和扩展,以适应复杂的业务需求。 - **社区强大**:作为 Spring 家族的一员,拥有庞大的社区和丰富的文档。 - **对 Spring 生态无缝集成**:与 Spring MVC、Spring Boot 等框架结合得非常好。 简单来说,如果在用 Spring 框架开发 Web 应用,并且需要管理用户登录、权限控制,那么 Spring Security 就是我们最佳的标准选择。 #### 2、原理 Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先了解过滤器的一般作用是有帮助的。下图显示了单个 HTTP 请求的处理器的典型分层结构。  客户端向应用程序发送请求,容器创建一个 `FilterChain` ,其中包含基于请求 URI 路径应处理 `HttpServletRequest` 的 `Filter` 实例和 `Servlet` 。在 Spring MVC 应用程序中, `Servlet` 是 `DispatcherServlet` 的一个实例。最多只有一个 `Servlet` 可以处理单个 `HttpServletRequest` 和 `HttpServletResponse` 。 在`DefaultSecurityFilterChain`类中会默认加载内置的过滤器形成过滤器链  下面是一些主要的过滤器及其作用 | 过滤器名称 | 主要作用 | 关键说明 | | :----------------------------------- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DisabledEncodeUrlFilter | 防止 Session ID 在 URL 中泄露 | 防止在 URL 中编码 JSESSIONID,避免安全风险。 | | SecurityContextPersistenceFilter | 在请求之间存储安全上下文 | 在请求开始时,从 Session 中获取认证信息(如登录用户)并存入`SecurityContextHolder`;在请求结束后,清空 `SecurityContextHolder`,并可能将上下文保存回 Session。**这是实现“登录状态保持”的关键。** | | LogoutFilter | 处理注销请求 | 匹配指定的注销 URL(如`/logout`),清除用户的认证信息,并使 HttpSession 无效。 | | UsernamePasswordAuthenticationFilter | 处理表单登录认证 | 拦截登录请求(默认是 POST`/login`),尝试从请求中获取用户名和密码,并进行认证。认证成功后将用户信息存入安全上下文。 | | DefaultLoginPageGeneratingFilter | 生成默认登录页面 | 如果没有配置自定义登录页,这个过滤器会自动生成一个简单的 HTML 登录页面。 | | DefaultLogoutPageGeneratingFilter | 生成默认注销页面 | 自动生成一个简单的注销确认页面。 | | BasicAuthenticationFilter | 处理 HTTP Basic 认证 | 解析请求头中的`Authorization: Basic ...` 信息,进行身份认证。常用于 REST API。 | | RememberMeAuthenticationFilter | 处理“记住我”功能 | 检查请求中携带的“记住我” Cookie。如果用户尚未认证但存在有效的记住我 Cookie,则自动为其登录。 | | AnonymousAuthenticationFilter | 配置匿名用户 | 如果请求到达此处时用户仍未认证,则将其视为一个**匿名用户**,并赋予一个匿名身份。这保证了安全上下文中始终存在一个主体(Principal),方便统一处理。 | | SessionManagementFilter | 管理会话 | 处理与会话相关的安全策略,例如防止会话固定攻击(Session Fixation Attack)和控制同一用户的并发登录数量。 | | ExceptionTranslationFilter | 转换安全异常 | **这是链中一个至关重要的过滤器。** 它不进行认证授权,而是捕获链中后续过滤器(主要是 `FilterSecurityInterceptor`)抛出的异常,并将其转换为对应的 HTTP 响应。例如: • 捕获 `AccessDeniedException`(访问被拒绝) -> 返回 403 错误或跳转到指定页面。 • 捕获 `AuthenticationException`(认证失败) -> 跳转到登录页面。 | | FilterSecurityInterceptor | 进行访问授权决策 | **这是授权操作的最终关卡。** 它根据系统配置的权限规则(访问某个 URL 需要的角色或权限),对当前请求进行判断。如果用户有权访问,则放行请求到你的业务控制器;如果无权,则抛出异常(由 `ExceptionTranslationFilter` 处理)。 | ## 二、在SpringBoot中使用 选择起步依赖(仅供测试所以仅选需要的) 构建完成后启动会输出默认账号`user`的随机密码 ## 三、测试环境 #### 1、创建Controller 在`controller`包下创建`TestController.java` ```java import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/test") public class TestController { @RequestMapping("/hello") public String hello() { return "Hello security,I am doooog."; } } ``` 启动后访问`http://localhost:8080/api/test/hello`会发现被转跳到了登录页面 #### 2、登录后访问 默认的账号为`user`,默认随机密码会控制台输出 #### 3、默认密码怎么来的 通过编译输出可以找到`SecurityAutoConfiguration`这个类 24行表示默认配置文件为`SecurityProperties`类提供 类中可以找到默认的静态内部用户类,定义了账号密码的生成 #### 4、自定义用户 可以修改SpringBoot配置文件来设置用户密码 ```properties spring.security.user.name=doooog spring.security.user.password=123456 ``` 或者使用`yml`格式 ```yml spring: security: user: name:doooog password:123456 ``` 重新运行项目后即可使用上面的账号密码登录 #### 5、注销登录 只需访问`http://localhost:8080/logout`即可退出 ## 四、基于内存的用户认证 创建`SecurityConfig`配置类 新建`util`包,创建`SecurityConfig.java` ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public UserDetailsService userDetailsService() { // 创建基于内存的用户管理器 InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); inMemoryUserDetailsManager.createUser( // 创建UserDetail 对象,用于管理用户名、用户密码、用户角色、用户权限 // 注意:withDefaultPasswordEncoder() 方法是不安全的,仅用于示例 User.withDefaultPasswordEncoder() .username("doooog") .password("123456") .roles("USER") .build()); return inMemoryUserDetailsManager; } } ``` ## 五、基于数据库的用户认证 #### 1、添加数据库相关起步依赖 #### 2、添加`MyBatis-Plus`依赖 ```xml com.baomidou mybatis-plus-boot-starter 3.5.14 org.mybatis mybatis-spring org.mybatis mybatis-spring 3.0.5 ``` **注意**:内嵌MyBatis版本可能会出现冲突,这里选择自定义版本安装。 #### 3、添加配置 关闭`MyBatis-Plus`中的大写字母转换 ```properties # Mybatis-Plus mybatis-plus.configuration.map-underscore-to-camel-case=false ``` 在配置文件中添加对数据库的配置 ```properties spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MySQL spring.datasource.url=jdbc:mysql://127.0.0.1:3306/database?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 spring.datasource.username=username spring.datasource.password=password # Redis spring.data.redis.host=localhost spring.data.redis.port=6379 # 0~16 spring.data.redis.database=0 # spring.data.redis.username=username # spring.data.redis.password=password ``` #### 4、创建数据库表 在数据库中创建`user`表来存放用户信息 ```sql CREATE TABLE `user`( `id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', `username` VARCHAR(50) DEFAULT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(500) DEFAULT NULL COMMENT '密码', `enabled` BOOLEAN NOT NULL COMMENT '是否启用' ) CHARSET 'utf8'; ``` #### 5、创建`po`对象 新建`po`包,创建`User.java` ```java import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private Boolean enabled; } ``` #### 6、创建`mapper`接口 新建`mapper`包,创建`UserMapper.java`接口 ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import cn.doooog.security.po.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper { } ``` #### 7、创建`service`层 新建`service`包,创建`UserService.java`接口 ```java import cn.doooog.security.po.User; import com.baomidou.mybatisplus.extension.service.IService; public interface UserService extends IService { } ``` 新建`./impl`包,创建`UserServiceImpl.java` ```java import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import cn.doooog.security.mapper.UserMapper; import cn.doooog.security.po.User; import cn.doooog.security.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl implements UserService { } ``` #### 8、实现`UserDetailsManager`接口 在`util`包中创建`DBUserDetailsManager.java`类 ```java import cn.doooog.security.po.User; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; 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.Component; import java.util.ArrayList; import java.util.List; @Component public class DBUserDetailsManager implements UserDetailsService { @Autowired private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userService.getOne(queryWrapper); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } // 用户角色权限信息 List authorities = new ArrayList<>(); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getEnabled(), true, // 账户是否未过期 true, // 密码是否未过期 true, // 账户是否未被锁定 authorities); } } ``` #### 9、用户创建与密码加密 ##### 常见加密方式对比 | 特性 | 对称加密 | 非对称加密 | 哈希加密 | | :------------- | :------------------- | :--------------------------------- | :--------------------------- | | **密钥数量** | **一个**(共享密钥) | **两个**(公钥和私钥) | **零个**(无需密钥) | | **加密与解密** | **可逆的** | **可逆的** | **不可逆的**(单向) | | **核心用途** | **保密性** | **保密性、身份验证、数字签名** | **数据完整性验证、密码存储** | | **速度** | **快** | **慢**(比对称加密慢1000倍以上) | **非常快** | | **常见算法** | AES, DES, 3DES | RSA, ECC, DSA | SHA-256, MD5, bcrypt | | **经典比喻** | **同一把钥匙的锁** | **投递信箱(公钥公开,私钥私有)** | **指纹或数字摘要** | 其中**哈希加密**常用来加密存储的用户密码。 在`Spring Security`中默认使用的`PasswordEncoder`要求数据库中的密码格式为:`{id}password` 。它会根据id去判断密码的 加密方式,例如`{bcrypt}$2a$10$E9nLEy89Y6VKp2r5pitI5.HGbSVOs1s1g3/4oPqg6C4zw7jV6C1iK`代表被加密后的`123456`。 但这样会使数据库中的密码直接暴露加密的方式,所以一般直接在程序中指定默认的加密方式即可,修改配置文件: ```java @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } ``` ##### 编写新增用户方法 在`UserService.java`中添加方法 ```java int addUser(User user); ``` 在`UserServiceImpl.java`中实现方法 ```java import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import cn.doooog.security.mapper.UserMapper; import cn.doooog.security.po.User; import cn.doooog.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl implements UserService { @Autowired PasswordEncoder passwordEncoder; @Autowired UserMapper userMapper; @Override public int addUser(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); user.setEnabled(true); return userMapper.insert(user); } } ``` ##### 添加对应API 在`controller`包中创建`UserController.java`类 ```java import cn.doooog.security.po.User; import cn.doooog.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; 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; @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/add") public int addUser(@RequestBody User user){ return userService.addUser(user); } } ``` 如果直接访问该路径,将会出现`401 Unauthorized`错误,需要在`SpringSecurity`配置过滤器放行白名单 ## 六、URL路径放行 在`SecurityConfig.java`中添加 ```java @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable); http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/user/add").permitAll() .requestMatchers("/login", "/login.html", "/login/**").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**", "/static/**").permitAll() .requestMatchers("/error").permitAll() .anyRequest().authenticated() ); // 添加表单登录配置 http.formLogin(AbstractAuthenticationFilterConfigurer::permitAll); return http.build(); } ``` 之后进行测试  返回内容为`1`,则说明添加成功  ## 七、前后端分离 #### 1、依赖配置 需要在起步依赖中添加模板依赖,或者使用前端路由实现登录页面转跳 添加JWT依赖 ```xml io.jsonwebtoken jjwt-api 0.12.6 io.jsonwebtoken jjwt-impl 0.12.6 runtime io.jsonwebtoken jjwt-jackson 0.12.6 runtime ``` #### 2、页面设置 ##### 登录页 在资源目录下创建`templates/login.html` ```html Title 用户名: 密码: ``` ##### 登录后端路由 创建`controller/AuthController.java` ```java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class AuthController { @GetMapping("/api/login") public String login() { return "login"; } } ``` ##### 鉴权测试页面 ```java import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/test") public class TestController { @RolesAllowed("USER") @RequestMapping("/hello") public String hello() { return "hello world"; } @RolesAllowed("ADMIN") @RequestMapping("/index") public String index() { return "index"; } } ``` #### 3、持久层对象 ##### 用户数据 创建`po/User.java` ```java import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; @Data @TableName("user") public class User implements Serializable { @TableId private Integer id; private String account; private String password; private String role; } ``` ##### 返回结果 创建`vo/ResponseVO.java` ```java import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class ResponseVO { private Integer code; private String msg; private T data; public static ResponseVO success(T data) { ResponseVO result = new ResponseVO<>(); result.setCode(200); result.setMsg("success"); result.setData(data); return result; } public static ResponseVO success(String msg, T data) { ResponseVO result = new ResponseVO<>(); result.setCode(200); result.setMsg(msg); result.setData(data); return result; } public static ResponseVO failed(String msg) { return error(203, msg); } public static ResponseVO error(Integer code, String msg) { ResponseVO result = new ResponseVO<>(); result.setCode(code); result.setMsg(msg); result.setData("{}"); return result; } public static ResponseVO error(String msg) { return error(400, msg); } public static ResponseVO denyAccess(String msg) { return error(403, msg); } } ``` #### 4、实现原理 前后端不同的环境决定了需要自定义登录过滤器的大部分,下面是流程图。 ##### 需要实现接口列表 **EntryPoint**:未登录用户访问受保护资源时触发 **Login**:自定义认证逻辑,验证用户名密码 **Success**:登录成功后的处理 **Failure**:登录失败后的处理 **AccessDeny**:已登录但权限不足时触发 **SessionInformationExpiredStrategy**:检测到异地登录时触发(最多 1 个会话) **Logout**:用户注销时的处理 #### 5、用户服务实现 创建`dao/UserMapper.java` ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import cn.doooog.securitydome.po.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper { } ``` 创建`service/UserService.java`接口 ```java import org.springframework.security.core.userdetails.UserDetailsService; public interface UserService extends UserDetailsService { } ``` 创建`service/impl/UserServiceImpl.java` ```java import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.List; @Service public class UserServiceImpl extends ServiceImpl implements UserService, UserDetailsService { private final UserMapper userMapper; @Autowired public UserServiceImpl(UserMapper userMapper) { this.userMapper = userMapper; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("phone", username).or().eq("email", username); List list = userMapper.selectList(wrapper); // 不用selectOne因为会抛异常 if (list == null || list.isEmpty()) throw new UsernameNotFoundException("用户不存在!"); User user = list.get(0); // 只选第一个 if (user == null) throw new UsernameNotFoundException("用户名错误!"); if (user.getStatus() == 1) throw new UsernameNotFoundException("用户被禁用!"); return buildUserDetails(user); } /** * 根据用户ID加载用户详情(用于JWT验证) */ public UserDetails loadUserByUserId(Integer userId) throws UsernameNotFoundException { User user = userMapper.selectById(userId); if (user == null) throw new UsernameNotFoundException("用户不存在!"); if (user.getStatus() == 1) throw new UsernameNotFoundException("用户被禁用!"); return buildUserDetails(user); } /** * 构建 UserDetails 对象 */ private UserDetails buildUserDetails(User user) { // 获取用户权限,并把其添加到GrantedAuthority中 // 添加默认权限 List grantedAuthorities = new ArrayList<>(); if (user.getRole() == null) { user.setRole("ROLE_USER"); } // 添加多项权限 String[] roles = user.getRole().split(","); if (roles.length != 0) { for (String role : roles) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role); grantedAuthorities.add(grantedAuthority); } } else { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(user.getRole()); grantedAuthorities.add(grantedAuthority); } return new org.springframework.security.core.userdetails.User(user.getUserId() + "", user.getPasswordHash(), grantedAuthorities); } } ``` #### 6、EntryPoint 登录鉴权入口,创建`auth/EntryPoint.java` ```java import cn.doooog.dshop.vo.ResponseVO; import com.google.gson.Gson; 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.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class EntryPoint implements AuthenticationEntryPoint { private final Gson gson; @Autowired public EntryPoint(Gson gson) { this.gson = gson; } //未登录时返回给前端数据 @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setStatus(401); ResponseVO result = ResponseVO.failed("鉴权失败"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 7、Login 自定义认证逻辑处理,创建`auth/Login.java` ```java import cn.doooog.securitydome.service.impl.UserServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; @Component public class Login implements AuthenticationProvider { private final UserServiceImpl userServiceImpl; private final BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired public Login(UserServiceImpl userServiceImpl, BCryptPasswordEncoder bCryptPasswordEncoder) { this.userServiceImpl = userServiceImpl; this.bCryptPasswordEncoder = bCryptPasswordEncoder; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String account = authentication.getName(); //获取用户名 String password = (String) authentication.getCredentials(); //获取密码 UserDetails userDetails = userServiceImpl.loadUserByUsername(account); boolean checkPassword = bCryptPasswordEncoder.matches(password, userDetails.getPassword()); if (!checkPassword) { throw new BadCredentialsException("密码不正确,请重新登录!"); } return new UsernamePasswordAuthenticationToken(account, password, userDetails.getAuthorities()); } @Override public boolean supports(Class> aClass) { return true; } } ``` #### 8、Success 登录成功处理逻辑,创建`auth/Success.java` ```java import cn.doooog.dshop.util.JwtUtil; import com.google.gson.Gson; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Component public class Success implements AuthenticationSuccessHandler { private static final Logger log = LoggerFactory.getLogger(Success.class); private final Gson gson; private final JwtUtil jwtUtil; @Autowired public Success(Gson gson, JwtUtil jwtUtil) { this.gson = gson; this.jwtUtil = jwtUtil; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //登录成功时返回给前端的数据 log.info("用户登录成功: {}, 权限: {}", authentication.getName(), authentication.getAuthorities()); // 生成 JWT token String userId = authentication.getName(); String token = jwtUtil.generateToken(userId, userId); // 构建返回数据 Map data = new HashMap<>(); data.put("token", token); data.put("userId", userId); ResponseVO> result = ResponseVO.success("登录成功", data); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 9、Failure 登录失败处理逻辑,创建`auth/Failure.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; 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.BadCredentialsException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class Failure implements AuthenticationFailureHandler { private final Gson gson; @Autowired public Failure(Gson gson) { this.gson = gson; } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { ResponseResult responseResult; if (e instanceof UsernameNotFoundException) { responseResult = ResponseResult.fail(e.getMessage()); } else if (e instanceof BadCredentialsException) { responseResult = ResponseResult.fail("密码错误!!"); } else { responseResult = ResponseResult.fail(e.getMessage()); } //处理编码方式,防止中文乱码的情况 response.setContentType("text/json;charset=utf-8"); //返回给前台 response.getWriter().write(gson.toJson(responseResult)); } } ``` #### 10、AccessDeny 鉴权失败处理逻辑,创建`auth/AccessDeny.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; 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.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class AccessDeny implements AccessDeniedHandler { private final Gson gson; @Autowired public AccessDeny(Gson gson) { this.gson = gson; } @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { ResponseResult result = ResponseResult.denyAccess("无权访问,need Authorities!!"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 11、SessionInformationExpiredStrategy 异地登录处理逻辑,创建`auth/SessionInformationExpiredStrategy.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.session.SessionInformationExpiredEvent; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class SessionInformationExpiredStrategy implements org.springframework.security.web.session.SessionInformationExpiredStrategy { private final Gson gson; @Autowired public SessionInformationExpiredStrategy(Gson gson) { this.gson = gson; } @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { ResponseResult result = ResponseResult.fail("您的账号在异地登录,建议修改密码"); HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 12、Logout 退出登录处理逻辑,创建`auth/Logout.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; 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.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class Logout implements LogoutSuccessHandler { private final Gson gson; @Autowired public Logout(Gson gson) { this.gson = gson; } @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { ResponseResult result = ResponseResult.success("注销成功"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 13、SessionConfig 全局Session配置,创建`config/SessionConfig.java` ```java import org.springframework.boot.autoconfigure.session.DefaultCookieSerializerCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.MapSessionRepository; import org.springframework.session.SessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import java.util.concurrent.ConcurrentHashMap; @Configuration @EnableSpringHttpSession public class SessionConfig { @Bean public SessionRepository sessionRepository() { return new MapSessionRepository(new ConcurrentHashMap<>()); } @Bean DefaultCookieSerializerCustomizer cookieSerializerCustomizer() { return cookieSerializer -> { cookieSerializer.setSameSite("None"); cookieSerializer.setUseSecureCookie(true); // 此项必须,否则set-cookie会被chrome浏览器阻拦 }; } } ``` #### 14、PasswordEncoderConfig 自定义密码加密配置,创建`config/PasswordEncoderConfig.java` ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration public class PasswordEncoderConfig { @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` #### 15、JwtUtil JWT密钥生成,创建`auth/JwtUtil.java` ```java package cn.doooog.dshop.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JWT 工具类 */ @Component public class JwtUtil { @Value("${jwt.secret:dshop-secret-key-for-jwt-token-generation-must-be-at-least-256-bits}") private String secret; @Value("${jwt.expiration:86400000}") // 默认24小时 private Long expiration; /** * 生成密钥 */ private SecretKey getSignKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } /** * 生成 token */ public String generateToken(String userId, String username) { Map claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); return Jwts.builder() .claims(claims) .subject(userId) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expiration)) .signWith(getSignKey()) .compact(); } /** * 从 token 中获取所有声明 */ public Claims getClaimsFromToken(String token) { try { return Jwts.parser() .verifyWith(getSignKey()) .build() .parseSignedClaims(token) .getPayload(); } catch (Exception e) { return null; } } /** * 从 token 中获取用户ID */ public String getUserIdFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims != null ? claims.getSubject() : null; } /** * 验证 token 是否有效 */ public boolean validateToken(String token) { try { Claims claims = getClaimsFromToken(token); if (claims == null) { return false; } Date expiration = claims.getExpiration(); return expiration.after(new Date()); } catch (Exception e) { return false; } } /** * 判断 token 是否过期 */ public boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); if (claims == null) { return true; } Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { return true; } } } ``` #### 16、JsonAuthenticationFilter 登录时Json参数传入过滤器,创建`auth/JsonAuthenticationFilter.java` ```java import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.io.IOException; import java.io.InputStream; import java.util.Map; /** * 自定义JSON格式登录过滤器 */ public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("不支持认证方法: " + request.getMethod()); } // 判断请求的Content-Type if (request.getContentType() != null && request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) { // 处理JSON格式的登录请求 try { InputStream is = request.getInputStream(); Map authRequest = new ObjectMapper().readValue(is, Map.class); String account = authRequest.get("account"); String password = authRequest.get("password"); if (account == null) { account = ""; } if (password == null) { password = ""; } account = account.trim(); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(account, password); setDetails(request, authToken); return this.getAuthenticationManager().authenticate(authToken); } catch (IOException e) { throw new AuthenticationServiceException("未能解析认证请求体", e); } } else { // 使用原有的表单登录方式 return super.attemptAuthentication(request, response); } } } ``` #### 17、JwtAuthenticationFilter JWT 认证过滤器 从请求头中获取 token 并验证,创建`auth/JwtAuthenticationFilter.java` ```java import cn.doooog.dshop.service.user.impl.UserServiceImpl; import cn.doooog.dshop.util.JwtUtil; import jakarta.annotation.PostConstruct; import jakarta.annotation.security.PermitAll; 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.context.ApplicationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.io.IOException; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * JWT 认证过滤器 * 从请求头中获取 token 并验证 */ @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserServiceImpl userService; @Autowired private ApplicationContext applicationContext; private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final Set permitAllPaths = new HashSet<>(); @PostConstruct public void init() { RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); Map handlerMethods = mapping.getHandlerMethods(); for (Map.Entry entry : handlerMethods.entrySet()) { HandlerMethod handlerMethod = entry.getValue(); Method method = handlerMethod.getMethod(); Class> beanType = handlerMethod.getBeanType(); if (method.isAnnotationPresent(PermitAll.class) || beanType.isAnnotationPresent(PermitAll.class)) { RequestMappingInfo mappingInfo = entry.getKey(); if (mappingInfo.getPathPatternsCondition() != null) { mappingInfo.getPathPatternsCondition().getPatterns() .forEach(pattern -> permitAllPaths.add(pattern.getPatternString())); } else if (mappingInfo.getPatternsCondition() != null) { permitAllPaths.addAll(mappingInfo.getPatternsCondition().getPatterns()); } } } permitAllPaths.add("/api/login"); permitAllPaths.add("/api/logout"); permitAllPaths.add("/error"); permitAllPaths.add("/public/**"); permitAllPaths.add("/**.ico"); } private boolean isPublicPath(String requestPath) { return permitAllPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, requestPath)); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestPath = request.getRequestURI(); if (isPublicPath(requestPath)) { filterChain.doFilter(request, response); return; } // 从请求头获取 token String token = getTokenFromRequest(request); if (token != null && jwtUtil.validateToken(token)) { // 从 token 中获取用户ID String userId = jwtUtil.getUserIdFromToken(token); if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) { try { // 根据用户ID加载用户详情 UserDetails userDetails = userService.loadUserByUserId(Integer.valueOf(userId)); if (userDetails != null) { // 创建认证对象 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 设置到 Security 上下文 SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { // JWT验证失败,不设置认证信息 SecurityContextHolder.clearContext(); } } } filterChain.doFilter(request, response); } /** * 从请求头中获取 token */ private String getTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` #### 18、SpringSecurityConfig `SpringSecurity`全局配置,创建`config/SpringSecurityConfig.java` ```java import cn.doooog.dshop.auth.*; import cn.doooog.dshop.service.user.UserService; import jakarta.annotation.PostConstruct; import jakarta.annotation.security.PermitAll; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; @Configuration @EnableWebSecurity @EnableMethodSecurity(jsr250Enabled = true) // 启用jsr250权限注解 public class SpringSecurityConfig { private final EntryPoint entryPoint; private final Success authSuccess; private final Failure authFailure; private final Logout logout; private final AccessDeny accessDeny; private final SessionInformationExpiredStrategy sessionInformationExpiredStrategy; private final Login login; private final UserService userService; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired private ApplicationContext applicationContext; private final List permitAllPaths = new ArrayList<>(); @Autowired public SpringSecurityConfig(EntryPoint entryPoint, Success authSuccess, Failure authFailure, Logout logout, AccessDeny accessDeny, SessionInformationExpiredStrategy sessionInformationExpiredStrategy, Login selfAuthenticationProvider1, UserService userService1, JwtAuthenticationFilter jwtAuthenticationFilter) { this.entryPoint = entryPoint; // 认证入口 this.authSuccess = authSuccess; // 登录成功逻辑处理 this.authFailure = authFailure; // 登录失败逻辑处理 this.logout = logout; // 注销逻辑处理 this.accessDeny = accessDeny; // 无权访问逻辑处理 this.sessionInformationExpiredStrategy = sessionInformationExpiredStrategy; // 检测异地登录逻辑处理 this.login = selfAuthenticationProvider1; // 自定义认证逻辑处理 this.userService = userService1;// 用户服务 this.jwtAuthenticationFilter = jwtAuthenticationFilter; // JWT 认证过滤器 } @PostConstruct public void scanPermitAllPaths() { RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); Map handlerMethods = mapping.getHandlerMethods(); for (Map.Entry entry : handlerMethods.entrySet()) { HandlerMethod handlerMethod = entry.getValue(); Method method = handlerMethod.getMethod(); Class> beanType = handlerMethod.getBeanType(); if (method.isAnnotationPresent(PermitAll.class) || beanType.isAnnotationPresent(PermitAll.class)) { RequestMappingInfo mappingInfo = entry.getKey(); if (mappingInfo.getPathPatternsCondition() != null) { mappingInfo.getPathPatternsCondition().getPatterns() .forEach(pattern -> permitAllPaths.add(pattern.getPatternString())); } else if (mappingInfo.getPatternsCondition() != null) { permitAllPaths.addAll(mappingInfo.getPatternsCondition().getPatterns()); } } } permitAllPaths.add("/api/login"); permitAllPaths.add("/api/logout"); permitAllPaths.add("/error"); permitAllPaths.add("/public/**"); permitAllPaths.add("/**.ico"); } @Bean public UserDetailsService userDetailsService() { return this.userService; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public JsonAuthenticationFilter jsonAuthenticationFilter(AuthenticationManager authenticationManager) { JsonAuthenticationFilter filter = new JsonAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager); filter.setAuthenticationSuccessHandler(authSuccess); filter.setAuthenticationFailureHandler(authFailure); filter.setFilterProcessesUrl("/api/login"); return filter; } //授权 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { //cors()解决跨域问题,csrf()会与restful风格冲突,默认SpringSecurity是开启的,所以要disable()关闭一下 http.cors(AbstractHttpConfigurer::disable).csrf(AbstractHttpConfigurer::disable); //使用自定义认证逻辑处理 http.authenticationProvider(login); http.authorizeHttpRequests( // 授权 auth -> { for (String path : permitAllPaths) { auth.requestMatchers(path).permitAll(); } auth.anyRequest().authenticated(); }// 其他所有请求都需要认证 ).formLogin( //开启登录 //允许所有人访问 form -> form .loginPage("/api/login").permitAll() // 登录页面 .successHandler(authSuccess) // 登录成功逻辑处理 .failureHandler(authFailure) // 登录失败逻辑处理 .disable() // 禁用默认的表单登录,使用自定义的JSON过滤器 ).logout( //开启注销 logout -> logout.logoutUrl("/api/logout").permitAll() // 注销页面 .logoutSuccessHandler(this.logout) //注销逻辑处理 .deleteCookies("JSESSIONID") //删除cookie ).exceptionHandling( // 认证异常处理 exception -> exception.accessDeniedHandler(accessDeny) //权限不足的时候的逻辑处理 .authenticationEntryPoint(entryPoint) //未登录是的逻辑处理 ).sessionManagement( // session管理 session -> session.maximumSessions(1) // 限制用户同时登录个数 // .maxSessionsPreventsLogin(true) // 禁止多账号登录 .expiredSessionStrategy(sessionInformationExpiredStrategy) //异地登录的逻辑处理 ); // 添加自定义的JSON登录过滤器 http.addFilterAt(jsonAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class); // 添加 JWT 认证过滤器 - 在 UsernamePasswordAuthenticationFilter 之前执行 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } } ``` **注意**:如果设置了`maxSessionsPreventsLogin(true)`,在同一账号已经登录的情况下,其他地点登录不会成功,除非已登录地点退出登录。 **注意**:当某个路径不需要进行登录状态过滤时,可以在其Controller方法上添加`@PermitAll`注解进行放行,若某个路径需要某个权限才可以访问时,在其Controller方法上添加`@RoleAllowed("ADMIN")`,注解里的权限不需要添加`ROLE_`前缀,但数据库中的权限需要添加!! Loading... ## 一、SpringSecurity #### 1、简介 `Spring Security` 是一个高度可定制、功能强大且广泛使用的、为基于 Java 的企业应用提供认证、授权和其他安全功能的框架。 它的主要优点: - **功能全面**:覆盖了认证、授权、防护等方方面面。 - **高度可扩展**:几乎每一个组件都可以被定制和扩展,以适应复杂的业务需求。 - **社区强大**:作为 Spring 家族的一员,拥有庞大的社区和丰富的文档。 - **对 Spring 生态无缝集成**:与 Spring MVC、Spring Boot 等框架结合得非常好。 简单来说,如果在用 Spring 框架开发 Web 应用,并且需要管理用户登录、权限控制,那么 Spring Security 就是我们最佳的标准选择。 #### 2、原理 Spring Security 的 Servlet 支持基于 Servlet 过滤器,因此首先了解过滤器的一般作用是有帮助的。下图显示了单个 HTTP 请求的处理器的典型分层结构。  客户端向应用程序发送请求,容器创建一个 `FilterChain` ,其中包含基于请求 URI 路径应处理 `HttpServletRequest` 的 `Filter` 实例和 `Servlet` 。在 Spring MVC 应用程序中, `Servlet` 是 `DispatcherServlet` 的一个实例。最多只有一个 `Servlet` 可以处理单个 `HttpServletRequest` 和 `HttpServletResponse` 。 在`DefaultSecurityFilterChain`类中会默认加载内置的过滤器形成过滤器链  下面是一些主要的过滤器及其作用 | 过滤器名称 | 主要作用 | 关键说明 | | :----------------------------------- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | DisabledEncodeUrlFilter | 防止 Session ID 在 URL 中泄露 | 防止在 URL 中编码 JSESSIONID,避免安全风险。 | | SecurityContextPersistenceFilter | 在请求之间存储安全上下文 | 在请求开始时,从 Session 中获取认证信息(如登录用户)并存入`SecurityContextHolder`;在请求结束后,清空 `SecurityContextHolder`,并可能将上下文保存回 Session。**这是实现“登录状态保持”的关键。** | | LogoutFilter | 处理注销请求 | 匹配指定的注销 URL(如`/logout`),清除用户的认证信息,并使 HttpSession 无效。 | | UsernamePasswordAuthenticationFilter | 处理表单登录认证 | 拦截登录请求(默认是 POST`/login`),尝试从请求中获取用户名和密码,并进行认证。认证成功后将用户信息存入安全上下文。 | | DefaultLoginPageGeneratingFilter | 生成默认登录页面 | 如果没有配置自定义登录页,这个过滤器会自动生成一个简单的 HTML 登录页面。 | | DefaultLogoutPageGeneratingFilter | 生成默认注销页面 | 自动生成一个简单的注销确认页面。 | | BasicAuthenticationFilter | 处理 HTTP Basic 认证 | 解析请求头中的`Authorization: Basic ...` 信息,进行身份认证。常用于 REST API。 | | RememberMeAuthenticationFilter | 处理“记住我”功能 | 检查请求中携带的“记住我” Cookie。如果用户尚未认证但存在有效的记住我 Cookie,则自动为其登录。 | | AnonymousAuthenticationFilter | 配置匿名用户 | 如果请求到达此处时用户仍未认证,则将其视为一个**匿名用户**,并赋予一个匿名身份。这保证了安全上下文中始终存在一个主体(Principal),方便统一处理。 | | SessionManagementFilter | 管理会话 | 处理与会话相关的安全策略,例如防止会话固定攻击(Session Fixation Attack)和控制同一用户的并发登录数量。 | | ExceptionTranslationFilter | 转换安全异常 | **这是链中一个至关重要的过滤器。** 它不进行认证授权,而是捕获链中后续过滤器(主要是 `FilterSecurityInterceptor`)抛出的异常,并将其转换为对应的 HTTP 响应。例如:<br> • 捕获 `AccessDeniedException`(访问被拒绝) -> 返回 403 错误或跳转到指定页面。<br> • 捕获 `AuthenticationException`(认证失败) -> 跳转到登录页面。 | | FilterSecurityInterceptor | 进行访问授权决策 | **这是授权操作的最终关卡。** 它根据系统配置的权限规则(访问某个 URL 需要的角色或权限),对当前请求进行判断。如果用户有权访问,则放行请求到你的业务控制器;如果无权,则抛出异常(由 `ExceptionTranslationFilter` 处理)。 | ## 二、在SpringBoot中使用 选择起步依赖(仅供测试所以仅选需要的) <img src="https://pic.soki.me/file/piclist/image-20251123220024173.png" alt="image-20251123220024173" style="zoom: 50%;" style=""> 构建完成后启动会输出默认账号`user`的随机密码 <img src="https://pic.soki.me/file/piclist/image-20251123221312459.png" alt="image-20251123221312459" style="zoom:50%;" style=""> ## 三、测试环境 #### 1、创建Controller 在`controller`包下创建`TestController.java` ```java import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/test") public class TestController { @RequestMapping("/hello") public String hello() { return "Hello security,I am doooog."; } } ``` 启动后访问`http://localhost:8080/api/test/hello`会发现被转跳到了登录页面 <img src="https://pic.soki.me/file/piclist/image-20251123221938474.png" alt="image-20251123221938474" style="zoom:50%;" style=""> #### 2、登录后访问 默认的账号为`user`,默认随机密码会控制台输出 <img src="https://pic.soki.me/file/piclist/image-20251123222137296.png" alt="image-20251123222137296" style="zoom:50%;" style=""> #### 3、默认密码怎么来的 通过编译输出可以找到`SecurityAutoConfiguration`这个类 <img src="https://pic.soki.me/file/piclist/image-20251123223400298.png" alt="image-20251123223400298" style="zoom:50%;" style=""> 24行表示默认配置文件为`SecurityProperties`类提供 <img src="https://pic.soki.me/file/piclist/image-20251123223517601.png" alt="image-20251123223517601" style="zoom:50%;" style=""> 类中可以找到默认的静态内部用户类,定义了账号密码的生成 #### 4、自定义用户 可以修改SpringBoot配置文件来设置用户密码 ```properties spring.security.user.name=doooog spring.security.user.password=123456 ``` 或者使用`yml`格式 ```yml spring: security: user: name:doooog password:123456 ``` 重新运行项目后即可使用上面的账号密码登录 #### 5、注销登录 只需访问`http://localhost:8080/logout`即可退出 ## 四、基于内存的用户认证 创建`SecurityConfig`配置类 新建`util`包,创建`SecurityConfig.java` ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public UserDetailsService userDetailsService() { // 创建基于内存的用户管理器 InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager(); inMemoryUserDetailsManager.createUser( // 创建UserDetail 对象,用于管理用户名、用户密码、用户角色、用户权限 // 注意:withDefaultPasswordEncoder() 方法是不安全的,仅用于示例 User.withDefaultPasswordEncoder() .username("doooog") .password("123456") .roles("USER") .build()); return inMemoryUserDetailsManager; } } ``` ## 五、基于数据库的用户认证 #### 1、添加数据库相关起步依赖 <img src="https://pic.soki.me/file/piclist/image-20251123231004146.png" alt="image-20251123231004146" style="zoom:50%;" style=""> #### 2、添加`MyBatis-Plus`依赖 ```xml <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.14</version> <exclusions> <exclusion> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>3.0.5</version> </dependency> ``` **注意**:内嵌MyBatis版本可能会出现冲突,这里选择自定义版本安装。 #### 3、添加配置 关闭`MyBatis-Plus`中的大写字母转换 ```properties # Mybatis-Plus mybatis-plus.configuration.map-underscore-to-camel-case=false ``` 在配置文件中添加对数据库的配置 ```properties spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MySQL spring.datasource.url=jdbc:mysql://127.0.0.1:3306/database?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 spring.datasource.username=username spring.datasource.password=password # Redis spring.data.redis.host=localhost spring.data.redis.port=6379 # 0~16 spring.data.redis.database=0 # spring.data.redis.username=username # spring.data.redis.password=password ``` #### 4、创建数据库表 在数据库中创建`user`表来存放用户信息 ```sql CREATE TABLE `user`( `id` INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', `username` VARCHAR(50) DEFAULT NULL UNIQUE COMMENT '用户名', `password` VARCHAR(500) DEFAULT NULL COMMENT '密码', `enabled` BOOLEAN NOT NULL COMMENT '是否启用' ) CHARSET 'utf8'; ``` #### 5、创建`po`对象 新建`po`包,创建`User.java` ```java import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private Boolean enabled; } ``` #### 6、创建`mapper`接口 新建`mapper`包,创建`UserMapper.java`接口 ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import cn.doooog.security.po.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper<User> { } ``` #### 7、创建`service`层 新建`service`包,创建`UserService.java`接口 ```java import cn.doooog.security.po.User; import com.baomidou.mybatisplus.extension.service.IService; public interface UserService extends IService<User> { } ``` 新建`./impl`包,创建`UserServiceImpl.java` ```java import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import cn.doooog.security.mapper.UserMapper; import cn.doooog.security.po.User; import cn.doooog.security.service.UserService; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { } ``` #### 8、实现`UserDetailsManager`接口 在`util`包中创建`DBUserDetailsManager.java`类 ```java import cn.doooog.security.po.User; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; 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.Component; import java.util.ArrayList; import java.util.List; @Component public class DBUserDetailsManager implements UserDetailsService { @Autowired private UserService userService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userService.getOne(queryWrapper); if (user == null) { throw new UsernameNotFoundException("用户不存在"); } // 用户角色权限信息 List<GrantedAuthority> authorities = new ArrayList<>(); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getEnabled(), true, // 账户是否未过期 true, // 密码是否未过期 true, // 账户是否未被锁定 authorities); } } ``` #### 9、用户创建与密码加密 ##### 常见加密方式对比 | 特性 | 对称加密 | 非对称加密 | 哈希加密 | | :------------- | :------------------- | :--------------------------------- | :--------------------------- | | **密钥数量** | **一个**(共享密钥) | **两个**(公钥和私钥) | **零个**(无需密钥) | | **加密与解密** | **可逆的** | **可逆的** | **不可逆的**(单向) | | **核心用途** | **保密性** | **保密性、身份验证、数字签名** | **数据完整性验证、密码存储** | | **速度** | **快** | **慢**(比对称加密慢1000倍以上) | **非常快** | | **常见算法** | AES, DES, 3DES | RSA, ECC, DSA | SHA-256, MD5, bcrypt | | **经典比喻** | **同一把钥匙的锁** | **投递信箱(公钥公开,私钥私有)** | **指纹或数字摘要** | 其中**哈希加密**常用来加密存储的用户密码。 在`Spring Security`中默认使用的`PasswordEncoder`要求数据库中的密码格式为:`{id}password` 。它会根据id去判断密码的 加密方式,例如`{bcrypt}$2a$10$E9nLEy89Y6VKp2r5pitI5.HGbSVOs1s1g3/4oPqg6C4zw7jV6C1iK`代表被加密后的`123456`。 但这样会使数据库中的密码直接暴露加密的方式,所以一般直接在程序中指定默认的加密方式即可,修改配置文件: ```java @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } ``` ##### 编写新增用户方法 在`UserService.java`中添加方法 ```java int addUser(User user); ``` 在`UserServiceImpl.java`中实现方法 ```java import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import cn.doooog.security.mapper.UserMapper; import cn.doooog.security.po.User; import cn.doooog.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired PasswordEncoder passwordEncoder; @Autowired UserMapper userMapper; @Override public int addUser(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); user.setEnabled(true); return userMapper.insert(user); } } ``` ##### 添加对应API 在`controller`包中创建`UserController.java`类 ```java import cn.doooog.security.po.User; import cn.doooog.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; 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; @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/add") public int addUser(@RequestBody User user){ return userService.addUser(user); } } ``` 如果直接访问该路径,将会出现`401 Unauthorized`错误,需要在`SpringSecurity`配置过滤器放行白名单 ## 六、URL路径放行 在`SecurityConfig.java`中添加 ```java @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf(AbstractHttpConfigurer::disable); http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/user/add").permitAll() .requestMatchers("/login", "/login.html", "/login/**").permitAll() .requestMatchers("/css/**", "/js/**", "/images/**", "/static/**").permitAll() .requestMatchers("/error").permitAll() .anyRequest().authenticated() ); // 添加表单登录配置 http.formLogin(AbstractAuthenticationFilterConfigurer::permitAll); return http.build(); } ``` 之后进行测试  返回内容为`1`,则说明添加成功  ## 七、前后端分离 #### 1、依赖配置 需要在起步依赖中添加模板依赖,或者使用前端路由实现登录页面转跳 <img src="https://pic.soki.me/file/piclist/image-20251208085039175-1765155039459.png" alt="image-20251208085039175" style="zoom:50%;" style=""> 添加JWT依赖 ```xml <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.6</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.6</version> <scope>runtime</scope> </dependency> ``` #### 2、页面设置 ##### 登录页 在资源目录下创建`templates/login.html` ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> </head> <body> <!-- 简单登录 --> <form id="login" action="/api/login" method="post"> 用户名:<input type="text" name="username"> 密码:<input type="password" name="password"> <input type="button" value="登录" onclick="login()"> <script> function login() { const username = document.getElementsByName('username')[0].value; const password = document.getElementsByName('password')[0].value; console.log(username, password); if (username === '' || password === '') { alert('请填写用户名和密码'); return; } axios.post('/login', `username=${username}&password=${password}`).then(res => { if (res.data.code === 200) { alert('登录成功'); window.location.href = '/api/test/index'; } else { alert(res.data.responseResult); } }).catch(err => { console.log(err); }); } </script> </form> </body> </html> ``` ##### 登录后端路由 创建`controller/AuthController.java` ```java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class AuthController { @GetMapping("/api/login") public String login() { return "login"; } } ``` ##### 鉴权测试页面 ```java import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/test") public class TestController { @RolesAllowed("USER") @RequestMapping("/hello") public String hello() { return "hello world"; } @RolesAllowed("ADMIN") @RequestMapping("/index") public String index() { return "index"; } } ``` #### 3、持久层对象 ##### 用户数据 创建`po/User.java` ```java import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; @Data @TableName("user") public class User implements Serializable { @TableId private Integer id; private String account; private String password; private String role; } ``` ##### 返回结果 创建`vo/ResponseVO.java` ```java import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class ResponseVO<T> { private Integer code; private String msg; private T data; public static <T> ResponseVO<T> success(T data) { ResponseVO<T> result = new ResponseVO<>(); result.setCode(200); result.setMsg("success"); result.setData(data); return result; } public static <T> ResponseVO<T> success(String msg, T data) { ResponseVO<T> result = new ResponseVO<>(); result.setCode(200); result.setMsg(msg); result.setData(data); return result; } public static ResponseVO<Object> failed(String msg) { return error(203, msg); } public static ResponseVO<Object> error(Integer code, String msg) { ResponseVO<Object> result = new ResponseVO<>(); result.setCode(code); result.setMsg(msg); result.setData("{}"); return result; } public static ResponseVO<Object> error(String msg) { return error(400, msg); } public static ResponseVO<Object> denyAccess(String msg) { return error(403, msg); } } ``` #### 4、实现原理 前后端不同的环境决定了需要自定义登录过滤器的大部分,下面是流程图。 <img src="https://pic.soki.me/file/piclist/image-20251208090347304-1765155828869.png" alt="image-20251208090347304" style="zoom: 80%;" style=""> ##### 需要实现接口列表 **EntryPoint**:未登录用户访问受保护资源时触发 **Login**:自定义认证逻辑,验证用户名密码 **Success**:登录成功后的处理 **Failure**:登录失败后的处理 **AccessDeny**:已登录但权限不足时触发 **SessionInformationExpiredStrategy**:检测到异地登录时触发(最多 1 个会话) **Logout**:用户注销时的处理 #### 5、用户服务实现 创建`dao/UserMapper.java` ```java import com.baomidou.mybatisplus.core.mapper.BaseMapper; import cn.doooog.securitydome.po.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper<User> { } ``` 创建`service/UserService.java`接口 ```java import org.springframework.security.core.userdetails.UserDetailsService; public interface UserService extends UserDetailsService { } ``` 创建`service/impl/UserServiceImpl.java` ```java import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.List; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService { private final UserMapper userMapper; @Autowired public UserServiceImpl(UserMapper userMapper) { this.userMapper = userMapper; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("phone", username).or().eq("email", username); List<User> list = userMapper.selectList(wrapper); // 不用selectOne因为会抛异常 if (list == null || list.isEmpty()) throw new UsernameNotFoundException("用户不存在!"); User user = list.get(0); // 只选第一个 if (user == null) throw new UsernameNotFoundException("用户名错误!"); if (user.getStatus() == 1) throw new UsernameNotFoundException("用户被禁用!"); return buildUserDetails(user); } /** * 根据用户ID加载用户详情(用于JWT验证) */ public UserDetails loadUserByUserId(Integer userId) throws UsernameNotFoundException { User user = userMapper.selectById(userId); if (user == null) throw new UsernameNotFoundException("用户不存在!"); if (user.getStatus() == 1) throw new UsernameNotFoundException("用户被禁用!"); return buildUserDetails(user); } /** * 构建 UserDetails 对象 */ private UserDetails buildUserDetails(User user) { // 获取用户权限,并把其添加到GrantedAuthority中 // 添加默认权限 List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); if (user.getRole() == null) { user.setRole("ROLE_USER"); } // 添加多项权限 String[] roles = user.getRole().split(","); if (roles.length != 0) { for (String role : roles) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role); grantedAuthorities.add(grantedAuthority); } } else { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(user.getRole()); grantedAuthorities.add(grantedAuthority); } return new org.springframework.security.core.userdetails.User(user.getUserId() + "", user.getPasswordHash(), grantedAuthorities); } } ``` #### 6、EntryPoint 登录鉴权入口,创建`auth/EntryPoint.java` ```java import cn.doooog.dshop.vo.ResponseVO; import com.google.gson.Gson; 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.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class EntryPoint implements AuthenticationEntryPoint { private final Gson gson; @Autowired public EntryPoint(Gson gson) { this.gson = gson; } //未登录时返回给前端数据 @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setStatus(401); ResponseVO result = ResponseVO.failed("鉴权失败"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 7、Login 自定义认证逻辑处理,创建`auth/Login.java` ```java import cn.doooog.securitydome.service.impl.UserServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; @Component public class Login implements AuthenticationProvider { private final UserServiceImpl userServiceImpl; private final BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired public Login(UserServiceImpl userServiceImpl, BCryptPasswordEncoder bCryptPasswordEncoder) { this.userServiceImpl = userServiceImpl; this.bCryptPasswordEncoder = bCryptPasswordEncoder; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String account = authentication.getName(); //获取用户名 String password = (String) authentication.getCredentials(); //获取密码 UserDetails userDetails = userServiceImpl.loadUserByUsername(account); boolean checkPassword = bCryptPasswordEncoder.matches(password, userDetails.getPassword()); if (!checkPassword) { throw new BadCredentialsException("密码不正确,请重新登录!"); } return new UsernamePasswordAuthenticationToken(account, password, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> aClass) { return true; } } ``` #### 8、Success 登录成功处理逻辑,创建`auth/Success.java` ```java import cn.doooog.dshop.util.JwtUtil; import com.google.gson.Gson; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.HashMap; import java.util.Map; @Component public class Success implements AuthenticationSuccessHandler { private static final Logger log = LoggerFactory.getLogger(Success.class); private final Gson gson; private final JwtUtil jwtUtil; @Autowired public Success(Gson gson, JwtUtil jwtUtil) { this.gson = gson; this.jwtUtil = jwtUtil; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { //登录成功时返回给前端的数据 log.info("用户登录成功: {}, 权限: {}", authentication.getName(), authentication.getAuthorities()); // 生成 JWT token String userId = authentication.getName(); String token = jwtUtil.generateToken(userId, userId); // 构建返回数据 Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("userId", userId); ResponseVO<Map<String, Object>> result = ResponseVO.success("登录成功", data); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 9、Failure 登录失败处理逻辑,创建`auth/Failure.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; 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.BadCredentialsException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class Failure implements AuthenticationFailureHandler { private final Gson gson; @Autowired public Failure(Gson gson) { this.gson = gson; } @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { ResponseResult responseResult; if (e instanceof UsernameNotFoundException) { responseResult = ResponseResult.fail(e.getMessage()); } else if (e instanceof BadCredentialsException) { responseResult = ResponseResult.fail("密码错误!!"); } else { responseResult = ResponseResult.fail(e.getMessage()); } //处理编码方式,防止中文乱码的情况 response.setContentType("text/json;charset=utf-8"); //返回给前台 response.getWriter().write(gson.toJson(responseResult)); } } ``` #### 10、AccessDeny 鉴权失败处理逻辑,创建`auth/AccessDeny.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; 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.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class AccessDeny implements AccessDeniedHandler { private final Gson gson; @Autowired public AccessDeny(Gson gson) { this.gson = gson; } @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { ResponseResult result = ResponseResult.denyAccess("无权访问,need Authorities!!"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 11、SessionInformationExpiredStrategy 异地登录处理逻辑,创建`auth/SessionInformationExpiredStrategy.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.session.SessionInformationExpiredEvent; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class SessionInformationExpiredStrategy implements org.springframework.security.web.session.SessionInformationExpiredStrategy { private final Gson gson; @Autowired public SessionInformationExpiredStrategy(Gson gson) { this.gson = gson; } @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { ResponseResult result = ResponseResult.fail("您的账号在异地登录,建议修改密码"); HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 12、Logout 退出登录处理逻辑,创建`auth/Logout.java` ```java import cn.doooog.securitydome.vo.ResponseResult; import com.google.gson.Gson; 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.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class Logout implements LogoutSuccessHandler { private final Gson gson; @Autowired public Logout(Gson gson) { this.gson = gson; } @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { ResponseResult result = ResponseResult.success("注销成功"); response.setContentType("application/json;charset=utf-8"); response.getWriter().write(gson.toJson(result)); } } ``` #### 13、SessionConfig 全局Session配置,创建`config/SessionConfig.java` ```java import org.springframework.boot.autoconfigure.session.DefaultCookieSerializerCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.session.MapSessionRepository; import org.springframework.session.SessionRepository; import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; import java.util.concurrent.ConcurrentHashMap; @Configuration @EnableSpringHttpSession public class SessionConfig { @Bean public SessionRepository sessionRepository() { return new MapSessionRepository(new ConcurrentHashMap<>()); } @Bean DefaultCookieSerializerCustomizer cookieSerializerCustomizer() { return cookieSerializer -> { cookieSerializer.setSameSite("None"); cookieSerializer.setUseSecureCookie(true); // 此项必须,否则set-cookie会被chrome浏览器阻拦 }; } } ``` #### 14、PasswordEncoderConfig 自定义密码加密配置,创建`config/PasswordEncoderConfig.java` ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration public class PasswordEncoderConfig { @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } } ``` #### 15、JwtUtil JWT密钥生成,创建`auth/JwtUtil.java` ```java package cn.doooog.dshop.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JWT 工具类 */ @Component public class JwtUtil { @Value("${jwt.secret:dshop-secret-key-for-jwt-token-generation-must-be-at-least-256-bits}") private String secret; @Value("${jwt.expiration:86400000}") // 默认24小时 private Long expiration; /** * 生成密钥 */ private SecretKey getSignKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } /** * 生成 token */ public String generateToken(String userId, String username) { Map<String, Object> claims = new HashMap<>(); claims.put("userId", userId); claims.put("username", username); return Jwts.builder() .claims(claims) .subject(userId) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + expiration)) .signWith(getSignKey()) .compact(); } /** * 从 token 中获取所有声明 */ public Claims getClaimsFromToken(String token) { try { return Jwts.parser() .verifyWith(getSignKey()) .build() .parseSignedClaims(token) .getPayload(); } catch (Exception e) { return null; } } /** * 从 token 中获取用户ID */ public String getUserIdFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims != null ? claims.getSubject() : null; } /** * 验证 token 是否有效 */ public boolean validateToken(String token) { try { Claims claims = getClaimsFromToken(token); if (claims == null) { return false; } Date expiration = claims.getExpiration(); return expiration.after(new Date()); } catch (Exception e) { return false; } } /** * 判断 token 是否过期 */ public boolean isTokenExpired(String token) { try { Claims claims = getClaimsFromToken(token); if (claims == null) { return true; } Date expiration = claims.getExpiration(); return expiration.before(new Date()); } catch (Exception e) { return true; } } } ``` #### 16、JsonAuthenticationFilter 登录时Json参数传入过滤器,创建`auth/JsonAuthenticationFilter.java` ```java import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.io.IOException; import java.io.InputStream; import java.util.Map; /** * 自定义JSON格式登录过滤器 */ public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("不支持认证方法: " + request.getMethod()); } // 判断请求的Content-Type if (request.getContentType() != null && request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) { // 处理JSON格式的登录请求 try { InputStream is = request.getInputStream(); Map<String, String> authRequest = new ObjectMapper().readValue(is, Map.class); String account = authRequest.get("account"); String password = authRequest.get("password"); if (account == null) { account = ""; } if (password == null) { password = ""; } account = account.trim(); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(account, password); setDetails(request, authToken); return this.getAuthenticationManager().authenticate(authToken); } catch (IOException e) { throw new AuthenticationServiceException("未能解析认证请求体", e); } } else { // 使用原有的表单登录方式 return super.attemptAuthentication(request, response); } } } ``` #### 17、JwtAuthenticationFilter JWT 认证过滤器 从请求头中获取 token 并验证,创建`auth/JwtAuthenticationFilter.java` ```java import cn.doooog.dshop.service.user.impl.UserServiceImpl; import cn.doooog.dshop.util.JwtUtil; import jakarta.annotation.PostConstruct; import jakarta.annotation.security.PermitAll; 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.context.ApplicationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.io.IOException; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * JWT 认证过滤器 * 从请求头中获取 token 并验证 */ @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserServiceImpl userService; @Autowired private ApplicationContext applicationContext; private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final Set<String> permitAllPaths = new HashSet<>(); @PostConstruct public void init() { RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods(); for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) { HandlerMethod handlerMethod = entry.getValue(); Method method = handlerMethod.getMethod(); Class<?> beanType = handlerMethod.getBeanType(); if (method.isAnnotationPresent(PermitAll.class) || beanType.isAnnotationPresent(PermitAll.class)) { RequestMappingInfo mappingInfo = entry.getKey(); if (mappingInfo.getPathPatternsCondition() != null) { mappingInfo.getPathPatternsCondition().getPatterns() .forEach(pattern -> permitAllPaths.add(pattern.getPatternString())); } else if (mappingInfo.getPatternsCondition() != null) { permitAllPaths.addAll(mappingInfo.getPatternsCondition().getPatterns()); } } } permitAllPaths.add("/api/login"); permitAllPaths.add("/api/logout"); permitAllPaths.add("/error"); permitAllPaths.add("/public/**"); permitAllPaths.add("/**.ico"); } private boolean isPublicPath(String requestPath) { return permitAllPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, requestPath)); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestPath = request.getRequestURI(); if (isPublicPath(requestPath)) { filterChain.doFilter(request, response); return; } // 从请求头获取 token String token = getTokenFromRequest(request); if (token != null && jwtUtil.validateToken(token)) { // 从 token 中获取用户ID String userId = jwtUtil.getUserIdFromToken(token); if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) { try { // 根据用户ID加载用户详情 UserDetails userDetails = userService.loadUserByUserId(Integer.valueOf(userId)); if (userDetails != null) { // 创建认证对象 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 设置到 Security 上下文 SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { // JWT验证失败,不设置认证信息 SecurityContextHolder.clearContext(); } } } filterChain.doFilter(request, response); } /** * 从请求头中获取 token */ private String getTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } } ``` #### 18、SpringSecurityConfig `SpringSecurity`全局配置,创建`config/SpringSecurityConfig.java` ```java import cn.doooog.dshop.auth.*; import cn.doooog.dshop.service.user.UserService; import jakarta.annotation.PostConstruct; import jakarta.annotation.security.PermitAll; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; @Configuration @EnableWebSecurity @EnableMethodSecurity(jsr250Enabled = true) // 启用jsr250权限注解 public class SpringSecurityConfig { private final EntryPoint entryPoint; private final Success authSuccess; private final Failure authFailure; private final Logout logout; private final AccessDeny accessDeny; private final SessionInformationExpiredStrategy sessionInformationExpiredStrategy; private final Login login; private final UserService userService; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired private ApplicationContext applicationContext; private final List<String> permitAllPaths = new ArrayList<>(); @Autowired public SpringSecurityConfig(EntryPoint entryPoint, Success authSuccess, Failure authFailure, Logout logout, AccessDeny accessDeny, SessionInformationExpiredStrategy sessionInformationExpiredStrategy, Login selfAuthenticationProvider1, UserService userService1, JwtAuthenticationFilter jwtAuthenticationFilter) { this.entryPoint = entryPoint; // 认证入口 this.authSuccess = authSuccess; // 登录成功逻辑处理 this.authFailure = authFailure; // 登录失败逻辑处理 this.logout = logout; // 注销逻辑处理 this.accessDeny = accessDeny; // 无权访问逻辑处理 this.sessionInformationExpiredStrategy = sessionInformationExpiredStrategy; // 检测异地登录逻辑处理 this.login = selfAuthenticationProvider1; // 自定义认证逻辑处理 this.userService = userService1;// 用户服务 this.jwtAuthenticationFilter = jwtAuthenticationFilter; // JWT 认证过滤器 } @PostConstruct public void scanPermitAllPaths() { RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class); Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods(); for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) { HandlerMethod handlerMethod = entry.getValue(); Method method = handlerMethod.getMethod(); Class<?> beanType = handlerMethod.getBeanType(); if (method.isAnnotationPresent(PermitAll.class) || beanType.isAnnotationPresent(PermitAll.class)) { RequestMappingInfo mappingInfo = entry.getKey(); if (mappingInfo.getPathPatternsCondition() != null) { mappingInfo.getPathPatternsCondition().getPatterns() .forEach(pattern -> permitAllPaths.add(pattern.getPatternString())); } else if (mappingInfo.getPatternsCondition() != null) { permitAllPaths.addAll(mappingInfo.getPatternsCondition().getPatterns()); } } } permitAllPaths.add("/api/login"); permitAllPaths.add("/api/logout"); permitAllPaths.add("/error"); permitAllPaths.add("/public/**"); permitAllPaths.add("/**.ico"); } @Bean public UserDetailsService userDetailsService() { return this.userService; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public JsonAuthenticationFilter jsonAuthenticationFilter(AuthenticationManager authenticationManager) { JsonAuthenticationFilter filter = new JsonAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager); filter.setAuthenticationSuccessHandler(authSuccess); filter.setAuthenticationFailureHandler(authFailure); filter.setFilterProcessesUrl("/api/login"); return filter; } //授权 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { //cors()解决跨域问题,csrf()会与restful风格冲突,默认SpringSecurity是开启的,所以要disable()关闭一下 http.cors(AbstractHttpConfigurer::disable).csrf(AbstractHttpConfigurer::disable); //使用自定义认证逻辑处理 http.authenticationProvider(login); http.authorizeHttpRequests( // 授权 auth -> { for (String path : permitAllPaths) { auth.requestMatchers(path).permitAll(); } auth.anyRequest().authenticated(); }// 其他所有请求都需要认证 ).formLogin( //开启登录 //允许所有人访问 form -> form .loginPage("/api/login").permitAll() // 登录页面 .successHandler(authSuccess) // 登录成功逻辑处理 .failureHandler(authFailure) // 登录失败逻辑处理 .disable() // 禁用默认的表单登录,使用自定义的JSON过滤器 ).logout( //开启注销 logout -> logout.logoutUrl("/api/logout").permitAll() // 注销页面 .logoutSuccessHandler(this.logout) //注销逻辑处理 .deleteCookies("JSESSIONID") //删除cookie ).exceptionHandling( // 认证异常处理 exception -> exception.accessDeniedHandler(accessDeny) //权限不足的时候的逻辑处理 .authenticationEntryPoint(entryPoint) //未登录是的逻辑处理 ).sessionManagement( // session管理 session -> session.maximumSessions(1) // 限制用户同时登录个数 // .maxSessionsPreventsLogin(true) // 禁止多账号登录 .expiredSessionStrategy(sessionInformationExpiredStrategy) //异地登录的逻辑处理 ); // 添加自定义的JSON登录过滤器 http.addFilterAt(jsonAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class); // 添加 JWT 认证过滤器 - 在 UsernamePasswordAuthenticationFilter 之前执行 http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } } ``` **注意**:如果设置了`maxSessionsPreventsLogin(true)`,在同一账号已经登录的情况下,其他地点登录不会成功,除非已登录地点退出登录。 **注意**:当某个路径不需要进行登录状态过滤时,可以在其Controller方法上添加`@PermitAll`注解进行放行,若某个路径需要某个权限才可以访问时,在其Controller方法上添加`@RoleAllowed("ADMIN")`,注解里的权限不需要添加`ROLE_`前缀,但数据库中的权限需要添加!! 最后修改:2026 年 01 月 06 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 👆请简单粗暴地爱我。