登录鉴权
用户登录鉴权是构建安全 Web
应用的核心功能之一。本文将详细介绍如何使用 Spring Security
和 JWT(JSON Web Token)
实现用户登录鉴权功能。
方案包括以下核心模块:
1. 用户登录与认证。
2. JWT 生成与验证。
3. 权限控制与鉴权。
4. 安全配置与异常处理。
引入依赖
js
<!-- 依赖声明 -->
<dependencies>
<!-- 安全认证依赖模块 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-security</artifactId>
</dependency>
<!-- 验证码 -->
<dependency>
<groupId>com.xiaomayi</groupId>
<artifactId>xiaomayi-captcha</artifactId>
</dependency>
</dependencies>
安全配置
在 xiaomayi-common/xiaomayi-security
安全控制模块中已内置 SecurityConfig
安全配置文件:
js
package com.xiaomayi.security.config;
import com.xiaomayi.security.filter.JwtAuthenticationTokenFilter;
import com.xiaomayi.security.filter.ResponseFilter;
import com.xiaomayi.security.handler.*;
import lombok.AllArgsConstructor;
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.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
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.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.filter.CorsFilter;
import java.util.stream.Collectors;
/**
* <p>
* 安全认证配置类
* 特别提醒:新版本中WebSecurityConfigurerAdapter已启用,目前采用新的写法
* 注意:SpringSecurity 6 没有了需要继承类这个做法,但是需要配置注解
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableGlobalAuthentication
@AllArgsConstructor
public class SecurityConfig {
private final ResponseFilter responseFilter;
/**
* 用户认证
*/
private final UserDetailsService userDetailsService;
/**
* 自定义认证过滤器
*/
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 自定义认证失败处理
*/
private final AuthenticationEntryPointImpl authenticationEntryPoint;
/**
* 自定义认证成功处理器
*/
private final AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
/**
* 自定义认证失败处理器
*/
private final AuthenticationFailureHandlerImpl authenticationFailureHandler;
/**
* 自定义退出处理
*/
private final LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* 自定义正在退出处理
*/
private final LogoutHandlerImpl logoutHandler;
/**
* 自定义访问权限不足处理
*/
private final AccessDeniedHandlerImpl accessDeniedHandler;
/**
* 跨域过滤器
*/
private final CorsFilter corsFilter;
/**
* 取消ROLE_前缀
*/
@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
// Remove the ROLE_ prefix
return new GrantedAuthorityDefaults("");
}
/**
* 设置密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理器,登录的时候参数会传给 authenticationManager
*
* @param config 认证配置
* @return 返回结果
* @throws Exception 异常处理
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* 设置默认认证提供
* 调用loadUserByUsername获得UserDetail信息,在AbstractUserDetailsAuthenticationProvider里执行用户状态检查
*
* @return 返回结果
*/
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
// DaoAuthenticationProvider 从自定义的 userDetailsService.loadUserByUsername 方法获取UserDetails
authenticationProvider.setUserDetailsService(userDetailsService);
// 设置密码编辑器
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
/**
* 安全认证配置
*
* @param http 安全请求
* @throws Exception 异常处理
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 忽略数据源转对象数组
AntPathRequestMatcher[] requestMatchers = IgnoreUrlsProperties.getIgnoreUrls()
.stream()
.map(AntPathRequestMatcher::new)
.collect(Collectors.toSet())
.toArray(new AntPathRequestMatcher[]{});
// 关闭CSRF,前后端分离不需要CSRF保护
http.csrf(AbstractHttpConfigurer::disable)
// 请求认证
.authorizeHttpRequests(request -> {
// 第一部分:从YML配置文件读取,忽略公开访问URL集合
request.requestMatchers(requestMatchers).permitAll();
// 第二部分:直接此处写死放行配置,如登录、错误页面等,强制默认放行
// 允许所有OPTIONS请求
request.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 允许直接访问授权登录接口
.requestMatchers(HttpMethod.POST, "/login","/register").permitAll()
// 允许 SpringMVC 的默认错误地址匿名访问
.requestMatchers("/error").permitAll()
// 其他所有接口必须有Authority信息,Authority在登录成功后的UserDetailsImpl对象中默认设置“ROLE_USER”
.requestMatchers("/**").hasRole("ROLE_ADMIN");
// 除上述放行URL外,其他全部请求都必须认证授权
request.anyRequest().authenticated();
})
// 解决“X-Frame-Options“指令设为“deny“,地址无法访问问题
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
// 添加Web过滤器之前添加过滤器统一输出格式
.addFilterBefore(responseFilter, WebAsyncManagerIntegrationFilter.class) // 在 Web...过滤器之前添加过滤器
// 前后端分离是是无状态的,因此设置会话管理策略为无状态,即不创建和使用会话;基于token,所以不需要session,此处直接禁用
.sessionManagement(request -> {
request.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
.exceptionHandling(request -> {
// 添加无权限访问处理
request.accessDeniedHandler(accessDeniedHandler);
}).exceptionHandling(request -> {
// 添加未登录处理,如认证/授权异常
request.authenticationEntryPoint(authenticationEntryPoint);
})
// 添加退出处理器
.logout(request -> {
// 设置退出URL地址
request.logoutUrl("/logout")
// 退出成功处理器
.logoutSuccessHandler(logoutSuccessHandler)
// 正在退出处理器
.addLogoutHandler(logoutHandler);
})
.authenticationProvider(authenticationProvider())
// 添加自定义JWT验证过滤器,替代UsernamePasswordAuthenticationFilter
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 添加CROS跨域过滤器
.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
.addFilterBefore(corsFilter, LogoutFilter.class);
// 返回结果
return http.build();
}
}
请求参数
js
package com.xiaomayi.system.dto;
import com.xiaomayi.xss.annotation.Xss;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* <p>
* 用户登录DTO
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@Data
public class LoginDTO {
/**
* 登录账号
*/
@Schema(description = "登录账号")
@Xss(message = "登录账号不能包含脚本字符")
@NotBlank(message = "登录账号不能为空")
@Size(max = 50, message = "登录账号最多50个字符")
private String username;
/**
* 登录密码
*/
@Schema(description = "登录密码")
@Xss(message = "登录账号不能包含脚本字符")
@NotBlank(message = "登录密码不能为空")
@Size(min = 6, max = 12, message = "登录密码为6-12个字符")
private String password;
/**
* 验证码值
*/
@Schema(description = "验证码")
@NotBlank(message = "验证码不能为空")
@Size(min = 3, max = 6, message = "验证码为3-6个字符")
private String code;
/**
* 验证码KEY
*/
@NotBlank(message = "验证码KEY不能为空")
private String key;
}
路由接收
js
package com.xiaomayi.admin.controller;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.logger.annotation.LoginLog;
import com.xiaomayi.logger.annotation.RequestLog;
import com.xiaomayi.logger.enums.LoginType;
import com.xiaomayi.logger.enums.RequestType;
import com.xiaomayi.system.dto.LoginDTO;
import com.xiaomayi.admin.service.LoginService;
import com.xiaomayi.system.dto.RegisterDTO;
import com.xiaomayi.tenant.annotation.TenantIgnore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
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;
/**
* <p>
* 用户表 前端控制器
* </p>
*
* @author 小蚂蚁云团队
* @since 2024-05-21
*/
@RestController
@Tag(name = "登录", description = "登录")
@AllArgsConstructor
public class LoginController {
@Autowired
private LoginService loginService;
/**
* 用户登录
*
* @param loginDTO 参数
* @return 返回结果
*/
@TenantIgnore
@Operation(summary = "用户登录", description = "用户登录")
@LoginLog(title = "用户登录", type = LoginType.LOGIN)
@PostMapping("/login")
public R login(@RequestBody @Validated LoginDTO loginDTO) {
return loginService.login(loginDTO);
}
/**
* 获取验证码
*
* @return 返回结果
*/
@Operation(summary = "获取验证码", description = "获取验证码")
@RequestLog(title = "获取验证码", type = RequestType.OTHER)
@GetMapping("/captcha")
public R captcha() {
return loginService.captcha();
}
}
用户登录
项目本地部署完毕之后,在登录界面输入 账号
、密码
、验证码
等参数后。
点击登录即可,登录请求 JSON
数据如下:
js
{
"username": "admin",
"password": "123456",
"code": "TK3Z6",
"key": "54e66743-0617-46fd-9b7e-cc1044ac1ccd"
}
输出结果:
js
{
"code": 0,
"msg": "操作成功",
"data": {
"access_token": "eyJuYW1lIjoi5bCP6JqC6JqBIiwiYWxnIjoiSFMyNTYifQ.eyJqdGkiOiI1NjViMmNhOTg5ZmY0ZTM4YjJhZTg5NzJhM2QyNGE0NSIsImlzcyI6IuWwj-iaguiagSIsImlhdCI6MTc0MTIzODIyMywia2V5IjoidmFsdWUiLCJ1c2VyS2V5IjoidXNlclZhbHVlIiwiZXhwIjoxNzQxMzI0NjIzfQ.jClkMero9Wn6R6zS4qtlgBN4tpD9OrPcbUFtkX-m6R8"
},
"ok": true
}