从头开始搭建一个SpringBoot项目--SpringSecurity的配置
- 前言
- 本文的目标
- 使用到的依赖、Redis配置、通用返回实体类
- 依赖
- Redis
- 项目里的配置
- 通用返回实体
- Result
- ResultCode
- ResultUtil
- 配置文件
- 配置的目录结构
- Spring Security的配置信息
- SecurityConfig
- WebMVCConfig
- 用到的类及代码
- 自定义User
- 数据库底层操作
- UserDao及UserBeanDao.xml
- UserBeanDao
- UserBeanDao.xml
- 自己的业务逻辑层
- UserBeanServices
- UserBeanServicesImpl
- SecurityUserServices
- UserController
- 自定义异常
- CustomAuthenticationEntryPoint
- CustomAccessDeniedHandler
- 自定义过滤器实现Token登录免认证
- 简单思考
- 什么?你不知道Spring Security的FilterChain(过滤链)?
- 如何被Spring Security识别
- 放到什么地方
- JwtFilter
- 工具类
- RedisUtil
- TokenUtil
- ResponseUtils
- 配置完成后的前后端交互文档
- 前端的界面
- error.html -- 错误页
- index.html -- 用户首页
- login.html -- 登录页
- js说明
- application.yml
- User表SQL
- 操作演示
- 正常流程
- 登录
- 登陆后的操作
- 退出登录按钮
- 登录失败演示
- 部分异常流程
- 未登录时访问其他界面
- token过期或者错误点击按钮转到登录页重新登录
- 遇到的问题
- 未登录时访问其他界面自动转到登录页
- 登陆失败的处理不被调用
- 资源读取不到但项目目录下却有
- 总结
前言
本文需要有一定的Spring Security基础,最好是了解其基本体系结构以及核心组件,如果可以简单配置并运行该框架最好。为确保阅读本文时更加易于理解,可以优先阅读以下文章:
Mybatis的引入:从头开始搭建一个SpringBoot项目-整合MyBatis
日志记录模块:从头开始搭建一个SpringBoot项目-日志记录logback
前后端交互文档:从头开始搭建一个SpringBoot项目–Swagger2的配置
本文是在上述框架引入之后,再进行的Spring Security引入和配置。
与本文相关的是:
Token基本理解及工具类 - Java生成token的工具类(对称签名)
Spring Security的体系结构 – SpringSecurity官网的Architecture部分的翻译
Spring Security核心组件 – SpringSecurity官网的Servlet Authentication Architecture部分的翻译
Spring Security认证流程分析 – SpringSecurity身份认证流程分析
在后文中的相关部分中,你将还会看到上述文章的引用
本文的目标
阅读本文你将学会以下内容:
- 使用
Spring Security进行身份验证 token验证在Spring Security中的使用Spring Security自定义异常处理Spring Security自定义过滤器
使用到的依赖、Redis配置、通用返回实体类
依赖
这里的SpringBoot版本的信息在我之前的文章里: 从头开始搭建一个SpringBoot项目-整合MyBatis有说过,SpringBoot的版本是2.7.4。
<!-- SpringSecurity安全框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- token 工具类 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<!-- redis缓存框架 用于token认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.4</version>
</dependency>
Redis
项目里的配置
文章的末尾会又具体的yml文件的信息,这里只是提一下
spring:
redis:
host: localhost
port: 6379
至于使用Redis的原因,大家可以看看我之前的文章: Java生成token的工具类(对称签名)
这里就不再使用Session存储用户信息了,转为使用Redis。安装和在SpringBoot里的配置就不在这里赘述了,大家自行寻找教程配置安装即可。
通用返回实体
在调用接口的时候,大都返回的是code – 状态码、msg – 提示信息、data--数据,那么我们可以给它封装一下。

Result
接口通用的的返回
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@ToString
public class Result<T> {
//状态码
private Integer code;
//提示信息
private String msg;
//返回的数据
private T data;
}
ResultCode
返回的枚举类
@Getter
public enum ResultCode {
ERROR(-1,"未知错误"),
SUCCESS(10000,"操作成功"),
LOGINSUCCESS(500,"登录成功。"),
UORPWRONG(4000,"用户名或密码错误");
private final Integer code;
private final String msg;
ResultCode(Integer code , String msg) {
this.code = code;
this.msg = msg;
}
}
ResultUtil
用于获取返回实体
public class ResultUtil {
public static Result success(Object object){
Result result = new Result();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
result.setData(object);
return result;
}
public static Result success(ResultCode resultCode , Object object){
Result result = new Result();
result.setCode(resultCode.getCode());
result.setMsg(resultCode.getMsg());
result.setData(object);
return result;
}
public static Result success(ResultCode resultCode){
Result result = new Result();
result.setCode(resultCode.getCode());
result.setMsg(resultCode.getMsg());
return result;
}
public static Result success(){
Result result = new Result();
result.setCode(ResultCode.SUCCESS.getCode());
result.setMsg(ResultCode.SUCCESS.getMsg());
return success(null);
}
public static Result error(Integer code,String msg){
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
return result;
}
public static Result error(){
Result result = new Result();
result.setCode(ResultCode.ERROR.getCode());
result.setMsg(ResultCode.ERROR.getMsg());
return result;
}
public static Result error(ResultCode resultCode){
Result result = new Result();
result.setCode(resultCode.getCode());
result.setMsg(resultCode.getMsg());
return result;
}
public static Result error(String msg){
Result result = new Result();
result.setCode(ResultCode.ERROR.getCode());
result.setMsg(msg);
return result;
}
public static Result error(ResultCode resultCode , Object object){
Result result = new Result();
result.setCode(resultCode.getCode());
result.setMsg(resultCode.getMsg());
result.setData(object);
return result;
}
}
配置文件
配置的目录结构

DruidConfig和Swagger2Config在以下的文章里讲到过,这里就不再赘述了。
从头开始搭建一个SpringBoot项目-整合MyBatis
从头开始搭建一个SpringBoot项目–Swagger2的配置
Spring Security的配置信息
在低版本中Spring Security的配置文件通常是通过继承一个WebSecurityConfigurerAdapter的父类,来Spring Security文件的配置
如以下这样
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//构建认证管理器的配置
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//框架主要的配置
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//认证管理器的配置
}
}
但是在一些高版本的Spring Security已经不再使用上述方法,而是使用@Bean注解的方式来配置,如以下这样
SecurityConfig
Spring Security的配置信息放在这里。以bean的形式配置
@Configuration
public class SecurityConfig {
//权限异常的处理 多指访问非自己权限内的信息 本文搭建的并未用到
@Autowired
CustomAccessDeniedHandler customAccessDeniedHandler;
//认证失败的错误处理 密码错误 token异常等
@Autowired
CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
//自定义的过滤器
@Autowired
JwtFilter jwtFilter;
//数据库底层操作
@Autowired
SecurityUserServices userServices;
//加密器的设置
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
//没有用到禁用csrf拦截
httpSecurity.csrf().disable()
//session策略为不使用session 因为用的redis
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity
//不需要认证的请求 也就是不对以下路径做拦截
//但还是会经过过滤链 只是如果请求路径为以下中的任何一个则放行
.authorizeRequests()
.antMatchers(
//不拦截登录请求
"/user/login" ,
//不拦截静态资源文件
"/static/**",
//不拦截项目资源
"/pro/**",
//不拦截swagger相关的资源文件
"/v2/api-docs",
"/swagger-resources",
"/swagger-resources/configuration/*",
"/swagger-resources/configuration/ui",
//不拦截swagger界面
"/swagger-ui.html").permitAll()
//其他所有的都需要认证
.anyRequest().authenticated();
//数据库的底层操作
httpSecurity.userDetailsService(userServices);
//将自定义过滤器加到账号密码验证过滤器之前
httpSecurity.addFilterBefore(jwtFilter , UsernamePasswordAuthenticationFilter.class);
//自定义异常处理一个是认证错误 一个是权限错误
httpSecurity.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler);
//添加跨域
httpSecurity.cors();
return httpSecurity.build();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// 不需要走过滤链的请求 也就是忽略过滤链 这里忽略druid 不然无法查看
return (web) -> web.ignoring()
.antMatchers("/druid/**" );
}
//配置跨源访问
@Bean
CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",
new CorsConfiguration().applyPermitDefaultValues());
return source;
}
//认证管理器 将其包装为Bean 用于自定义过滤器JwtFilter中的认证
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
WebMVCConfig
web的一些配置
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
//资源映射器
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//将地址栏输入的/pro映射到/static/pro
registry.addResourceHandler("/pro/**")
.addResourceLocations("classpath:/static/pro/");
}
//视图控制器
@Override
public void addViewControllers(ViewControllerRegistry registry) {
}
//跨域配置
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}
用到的类及代码
下面是整个用户的包结构,各个类的作用后面会具体讲到。
对于Mybatis的相关类,这里不再赘述。但是其中的实现,跟之前的文章略有出入,所以需要更改一下。

自定义User
在Spring Security框架中,User类是一个特殊的类,是框架默认的用户类。其中只包含了少数的数据项,诸如账号、密码、权限等,如果需要电话、邮件、地址等数据项,则需要自定义用户类。

如果需要自定义用户类,只需要实现UserDetails 接口即可,最好不要用User作为类名,否则很容易导错包。本项目自定义的用户类如下
@Setter
@Getter
@ToString
public class UserBean implements UserDetails {
@ApiModelProperty(value = "用户编号")
int id;
@ApiModelProperty(value = "用户姓名")
String name;
@ApiModelProperty(value = "用户密码")
private String pw;
@ApiModelProperty(value = "用户角色")
String role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//默认权限为空
return null;
}
@Override
public String getPassword() {
//数据库存储的不是密文 验证时需要使用密文 所以密码加密在
//SecurityUserServices里处理
return this.pw;
}
@Override
public String getUsername() {
return this.name;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
数据库底层操作
下面是数据库相关的代码及说明
UserDao及UserBeanDao.xml
底层的接口及其配置文件信息
UserBeanDao
public interface UserBeanDao {
//根据用户名称获取用户对象
UserBean getUserByUserName(String name);
//框架的的认证方式 - 这个
UserBean loadUserByUsername(String name);
}
UserBeanDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 这里指定接口 -->
<mapper namespace="com.demo.user.dao.UserBeanDao">
<select id="getUserByUserName" resultType="com.demo.user.bean.UserBean" parameterType="String">
select id , name , pw , role from user where name = #{name}
</select>
<!-- SpringSecurity框架认证的底层操作 必须包含用户名和密码 -->
<select id="loadUserByUsername" resultType="com.demo.user.bean.UserBean" parameterType="String">
select id , name , pw , role from user where name = #{name}
</select>
</mapper>
自己的业务逻辑层
用户的业务逻辑处理接口及实现类: UserBeanServices以及UserBeanServicesImpl
UserBeanServices
public interface UserBeanServices {
UserBean getUserByUserName(String name);
//登录接口
Result login(String userName , String password);
}
UserBeanServicesImpl
@Service
public class UserBeanServicesImpl implements UserBeanServices {
//在这里获取Spring Security的认证管理器 用于认证
@Autowired
private AuthenticationManager authenticationManager;
//redis的工具类 用于认证成功后向redis中存入登录信息
@Autowired
RedisUtil redisUtil;
//数据库底层操作
@Autowired
UserBeanDao dao;
@Override
public UserBean getUserByUserName(String name) {
return dao.getUserByUserName(name);
}
//登录接口 使用Spring Security的登录逻辑
@Override
public Result login(String userName, String password) {
//由Spring Security认证管理器实现认证
Authentication authentication = authenticationManager
.authenticate(
//将用户名和密码 包装成一个UsernamePasswordAuthenticationToken 用于认证
UsernamePasswordAuthenticationToken.unauthenticated(userName , password)
);
//将得到认证信息强转为用户实体
UserBean userBean = (UserBean) authentication.getPrincipal();
//以登陆的用户名获取token
String token = TokenUtil.getToken(userBean.getName());
//以键值对信息 向redis中存入 用户名 - 用户信息
redisUtil.setCacheObject("loginId:"+ userBean.getUsername() , userBean);
//将token和用户信息返回给前端
HashMap<String , Object> map = new HashMap<>();
map.put("token" , token);
map.put("user" , userBean);
System.out.println("登陆成功");
return ResultUtil.success(ResultCode.LOGINSUCCESS , map);
}
}
SecurityUserServices
在Spring Security框架中对于从数据库中取出用户的信息,也定义了一个接口
UserDetailsService,该类仅有一个方法:loadUserByUsername(String username),用于根据所给用户名查出用户信息。也就是我们之前目录结构的里面的SecurityUserServices
@Service
public class SecurityUserServices implements UserDetailsService {
//给Security提供的认证逻辑的底层
@Autowired
UserBeanDao userBeanDao;
//实现loadUserByUsername方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名取出信息
UserBean userBean = userBeanDao.loadUserByUsername(username);
//默认取出的密码是密文 因为我数据库里没加密 所以这里加密
userBean.setPw(new BCryptPasswordEncoder().encode(userBean.getPw()));
return userBean;
}
}
UserController
用户接口的控制器
@Api(tags = {"02 用户管理"} , position = 2)
@RestController
@RequestMapping("/user/")
public class UserController {
@Autowired
UserBeanServices userBeanServices;
@Autowired
RedisUtil redisUtil;
@ApiOperation(value = "根据名称获取用户信息" , notes = "用户名为字符串")
@PostMapping("/getUserBeanByName")
public Result getUserBeanByName(
@RequestParam("name") String name
) {
UserBean userBean = userBeanServices.getUserByUserName(name);
if(userBean != null)
return ResultUtil.success(ResultCode.SUCCESS , userBean);
else
return ResultUtil.success(ResultCode.ERROR);
}
@ApiOperation(value = "登录" , notes = "登录")
@PostMapping("/login")
public Result login(
@RequestParam("username") String name ,
@RequestParam("password") String password
) {
System.out.println("自定义登录逻辑");
return userBeanServices.login(name , password);}
@ApiOperation(value = "登出" , notes = "登出")
@PostMapping("/lg")
public Result lg() {
//清除redis里的信息
UserBean userBean = (UserBean) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SecurityContextHolder.clearContext();
//清除当前的信息
if (redisUtil.deleteObject("loginId:" + userBean.getUsername())) {
return ResultUtil.success("退出登录成功");
}else
return ResultUtil.error(ResultCode.ERROR);
}
}
自定义异常
在Spring Security中最常见的两中错误就是:
AuthenticationException:认证异常 - 密码错误、身份过期等。在这里由CustomAuthenticationEntryPoint处理
AccessDeniedException:拒绝连接异常 - 常见的是权限不够。在这里由CustomAccessDeniedHandler处理
对于前一种登陆时的错误,我们要返回提示信息,比如:账号或密码错误什么的。对于身份过期,我们要转到登录页,让他重新登陆。
第二种错误可以只返回一个提示信息,提醒他权限不够。
CustomAuthenticationEntryPoint
自定义认证异常处理,用于处理登录信息出错,身份过期等情况。
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
if(isAjaxRequest(request)){
//ajax请求的异常 一般是token过期
//返回状态码410 让前端转到登录页
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,authException.getMessage());
}
else {
if(authException instanceof BadCredentialsException
|| authException instanceof InternalAuthenticationServiceException
) {
//这里认证失败的错误处理 密码错误或者账号错误
System.out.println("账号或者密码错误");
String json = JSON.toJSONString(ResultUtil.error(ResultCode.UORPWRONG));
ResponseUtils.writMsgToResponse(response, json);
} else {
//防止地址栏输入 进入到另外的界面 这里直接重定向到登录页让用户登录
response.sendRedirect("/pro/html/login.html");
}
}
}
//判断是否是ajax请求
public static boolean isAjaxRequest(HttpServletRequest request) {
//部分版本可能没有 那么就需要发起请求时 自己设置一下请求头里的属性
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
}
CustomAccessDeniedHandler
自定义拒绝连接异常,用于处理权限错误。
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
//权限不够时的处理
Result result = ResultUtil.error("权限不够,请确认当前账户的身份。");
String json = JSON.toJSONString(result);
//处理异常
ResponseUtils.writMsgToResponse(response, json);
}
}
自定义过滤器实现Token登录免认证
假如要实现这样的一个功能:根据传来的token判断当前用户名的用户名信息是否存在于Redis中,如果存在则表示已登录,则不再需要认证。否则,则表示未登录,需要认证。
那我们应该怎么做呢?
简单思考
我们知道Spring Security其本质就是一系列的过滤器 -- Filter,我们可以在Spring Security的过滤链 -- FilterChain中加入一个过滤器来实现自定义的功能。那我们该怎么做?只需要思考以下两个问题:
- 如何放入到
Spring Security框架的FilterChain(过滤链)中 - 放入到
FilterChain中的中的什么位置
什么?你不知道Spring Security的FilterChain(过滤链)?
以下内容来自于本人翻译Spring官网里的Spring Security,翻译文章的地址为: SpringSecurity官网的Architecture部分的翻译
Security Filters(安全过滤器)
Security Filter 被SecurityFilterChain的API插入到FilterChainProxy中。这些Filter的顺序是很重要的。通常我们并不需要知道SpringSecurity中Filter的顺序。但有时候,知道顺序是有帮助的。
以下是SpringSecurity Filter的综合清单顺序:
//强制要求创建Session过滤器 用于强制生成Session
//与下面的SessionManagementFilter相关联
ForceEagerSessionCreationFilter
//通道过滤器 规定哪些请求必须走http协议 哪些走https协议
ChannelProcessingFilter
//Web异步管理集成过滤器
//此过滤器使得WebAsync异步线程能够获取到当前认证信息
WebAsyncManagerIntegrationFilter
//安全上下文存在过滤器
//控制SecurityContext的在一次请求中的生命周期,请求结束时清空,防止内存泄漏。
SecurityContextPersistenceFilter
//请求头写入过滤器 用来给相应添加一些header
HeaderWriterFilter
//跨域过滤器 一般用在跨域请求资源的时候
CorsFilter
//跨域请求伪造过滤器 用于防止csrf攻击
CsrfFilter
//登出过滤器 退出登录时的逻辑
LogoutFilter
//Oauth2请求鉴权重定向过滤器,需配合OAuth2.0的模块使用
OAuth2AuthorizationRequestRedirectFilter
//Saml2单点认证过滤器。需配合Spring Security SAML模块使用。
Saml2WebSsoAuthenticationRequestFilter
//X.509证书认证过滤器。
X509AuthenticationFilter
//预认证处理的抽象过滤器 自定义过滤器的基类
AbstractPreAuthenticatedProcessingFilter
//CAS 单点登录认证过滤器 。配合Spring Security CAS模块使用
CasAuthenticationFilter
//OAuth2 登录认证过滤器。处理通过 OAuth2 进行认证登录的逻辑
OAuth2LoginAuthenticationFilter
//SMAL 的 SSO 单点登录认证过滤器
Saml2WebSsoAuthenticationFilter
//用户名密码认证过滤器。 最主要的认证过滤器 账户的验证在这里进行
UsernamePasswordAuthenticationFilter
//OpenID认证过滤器。需要在依赖中依赖额外的相关模块才能启用它
OpenIDAuthenticationFilter
//默认登入页生成过滤器。默认 /login
DefaultLoginPageGeneratingFilter
//默认登出页生成过滤器。 默认 /logout
DefaultLogoutPageGeneratingFilter
//session管理,用于判断session是否过期。该过滤器可能会被多次执行
ConcurrentSessionFilter
//摘要认证过滤器。Web 应用程序中流行的可选的身份验证机制
DigestAuthenticationFilter
//Bearer标准token认证过滤器。
BearerTokenAuthenticationFilter
//标准认证过滤器 Web 应用程序中流行的可选的身份验证机制
//负责处理 HTTP 头中显示的基本身份验证凭据
BasicAuthenticationFilter
//请求缓存过滤器。主要作用是认证完成后恢复认证前的请求继续执行
RequestCacheAwareFilter
//安全上下文持有者感知请求过滤器 用于实现servlet的一些api
SecurityContextHolderAwareRequestFilter
//Jaas认证过滤器。适用于JAAS (Java 认证授权服务)
JaasApiIntegrationFilter
//记住我认证过滤器。处理“记住我”功能的过滤器。
RememberMeAuthenticationFilter
//匿名认证过滤器 如果访问不需要授权的资源 则以匿名身份访问
AnonymousAuthenticationFilter
//OAuth2授权码过滤器
OAuth2AuthorizationCodeGrantFilter
//Session管理器过滤
//其中SessionAuthenticationStrategy 用于管理 Session
SessionManagementFilter
//异常翻译过滤器 过滤链中的异常会等到了此处再一并处理
ExceptionTranslationFilter
//过滤器安全拦截器 这个过滤器决定了访问特定路径应该具备的权限
FilterSecurityInterceptor
//账户切换过滤器 用来做账户切换的
SwitchUserFilter
(上述信息参考:Spring Security详解一:过滤器)
其实简单的配置,只需要注意一个
过滤器 – UsernamePasswordAuthenticationFilter,用户名密码认证过滤器。用于验证用户的用户名和密码是否正确。这里后面会再次提到。
如何被Spring Security识别
要想被Spring Security识别,该过滤器可以继承OncePerRequestFilter
严谨的说只要继承了 Filter即可,但是在Spring体系中,推荐使用OncePerRequestFilter来实现,它可以确保一次请求只会通过一次该过滤器(Filter实际上并不能保证)
放到什么地方
先看JwtFilter的作用:判断当前用户名的用户名信息是否存在于Redis中,如果存在则表示已登录,则不再需要认证。否则,则表示未登录,需要认证。
那么这个过滤器根据其功能自然是要在具体认证发生之前的,还记得刚刚说的过滤链中用于具体认证的过滤器吗?没错,是它 – UsernamePasswordAuthenticationFilter,那么该自定义过滤器放到UsernamePasswordAuthenticationFilter之前即可。
也就对应了Spring Security配置文件中的
httpSecurity
.addFilterBefore(jwtFilter ,
UsernamePasswordAuthenticationFilter.class
);
JwtFilter
自定义的token认证过滤器
@Component
public class JwtFilter extends OncePerRequestFilter {
//redis工具类
@Autowired
private RedisUtil redisUtil;
//具体功能在此方法内写
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain)
throws ServletException, IOException {
//登录请求或者token为空则表示未登录 需要验证
String path = httpServletRequest.getServletPath();
System.out.println("请求地址:" + path);
String token = httpServletRequest.getHeader("token");
if (token == null|| "".equals(token) ||"/user/login".equals(path)) {
//无token或者登录 走UsernamePasswordAuthenticationFilter
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//解析token
String userid = null;
try {
//获取token中的用户名信息
userid = TokenUtil.getMsgFromToken(token);
} catch (ExpiredJwtException e) {
httpServletRequest.setAttribute("data","身份已过期,请重新登录。");
//直接重定向到错误信息界面
httpServletRequest.getRequestDispatcher("pro/html/error.html").forward(httpServletRequest,httpServletResponse);
return;
}catch (UnsupportedJwtException | MalformedJwtException | SignatureException e){
//否则的话 交给后面的过滤器处理
throw new RuntimeException("非法token");
}
//组成键 前缀可以自定义 最好具有一定地辨识度
String redisKey = "loginId:" + userid;
//从redis中获取用户信息
UserBean user = redisUtil.getCacheObject(redisKey);
if (Objects.isNull(user)) {
throw new RuntimeException("用户未登录");
}
//获取权限信息封装到Authentication中 表明已认证
SecurityContextHolder.getContext().setAuthentication(
UsernamePasswordAuthenticationToken.authenticated(user,
null,
user.getAuthorities())
);
//放行
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
工具类
RedisUtil
Redis工具类 用于简单的向Redis中存取数据
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
//设置token信息到redis中
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
//根据用户名从redis获取token信息
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
//删除单个对象
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
}
TokenUtil
用于生成和解析token,直接去下面找工具类即可
Java生成token的工具类(对称签名)
ResponseUtils
用于向Response中写入数据,主要是字符串。
public class ResponseUtils {
/**
* @description 向响应中写入信息
* @author 三文鱼先生
* @date 11:47 2022/12/2
* @param response
* @param string
* @return java.lang.String
**/
public static void writMsgToResponse(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
//字符编码
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e) {
e.printStackTrace();
}
}
}
配置完成后的前后端交互文档

尝试调用登录接口 如果看到以下信息内容 则表明成功引入Spring Security框架进行了验证。

前端的界面
既然项目搭好了 我们现在来搭建一下系统的界面,从前端走一下认证流程。
JQuery不会引入的话,参考以下文章:简单引入JQuery
前端的目录结构如下

error.html – 错误页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误信息页</title>
</head>
<body>
错误信息页
</body>
</html>
index.html – 用户首页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
登陆成功的首页
<button onClick = "test()">查看密码</button>
<button onClick = "logout()">退出登录的按钮</button>
<script src="../js/jquery-3.6.1.js"></script>
<script type="text/javascript">
// ajax 请求头 添加token
$.ajaxSetup({
beforeSend:function(XMLHttpRequest){
//添加ajax请求标识
XMLHttpRequest.setRequestHeader("X-Requested-With","XMLHttpRequest")
if(localStorage.getItem('token')!=''&&localStorage.getItem('token')!=undefined){
XMLHttpRequest.setRequestHeader("token",localStorage.getItem('token'))
}else{
XMLHttpRequest.setRequestHeader("token",'')
}
},
// ajax请求执行完毕后 再进行下面的函数 用来判断 传递给后端的 token 是否正确
complete:function(xhr,status){
if(xhr.status==401){
window.location.href="./login.html"
}
},
})
//判断token 重定向到登录页面
if(localStorage.getItem('token')==''
||localStorage.getItem('token')==undefined
||localStorage.getItem('token')=='null'){
window.location.href = "./login.html";
}
//测试按钮的监听事件
function test() {
$.ajax({
url: 'http://192.168.3.128:9800/user/getUserBeanByName',
type: 'POST',
dataType: 'json',
data: {
name : 'zs'
},
success: function(data){
console.log(data);
alert('密码:' + data.data.pw);
}
});
}
//登出按钮的监听事件
function logout() {
$.ajax({
url: 'http://192.168.3.128:9800/user/lg',
type: 'POST',
dataType: 'json',
data: {},
success: function(data){
console.log(data);
if(data.code == "10000") {
//删除浏览器存储的信息
localStorage.removeItem('token');
localStorage.removeItem('username');
window.location = 'login.html';
}else {
alert("遇到未知错误");
}
}
});
}
</script>
</body>
</html>
login.html – 登录页
<!DOCTYPE HTML>
<html>
<head>
<!-- <meta http-equiv="Access-Control-Allow-Origin" content="*">-->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js示例文件</title>
<input type="text" name="username" class="username" lay-verify="required" placeholder="请输入登录账号" maxlength="24"/>
</br>
<input type="password" name="password" class="password" lay-verify="required" placeholder="请输入密码" maxlength="20">
</br>
<button onClick = "login()">立即登录</button>
<script src="../js/jquery-3.6.1.js"></script>
<script type="text/javascript">
console.log($);
function login() {
var username = $(".username").val();
var password = $(".password").val();
console.log(username);
console.log(password);
$.ajax({
url: 'http://192.168.3.128:9800/user/' + 'login',
type: 'POST',
dataType: 'json',
data: {
username: username,
password: password
},
success: function (data) {
console.log(data);
if (data.code == "500") {
console.log('登陆成功');
console.log(data);
localStorage.setItem('username', data.data.user.username);
localStorage.setItem('token', data.data.token);
window.location = 'index.html';
} else if (data.code == "4000") {
alert("用户名或密码错误");
} else {
alert("登录失败");
}
},
error: function () {
alert("信息错误");
}
});
}
</script>
</head>
<body>
</body>
</html>
js说明
这里就不放js代码了,有需要的,自行去官网找资源即可。也可以参考以下文章: 简单引入JQuery
application.yml
最终运行的配置文件
server:
#配置端口及编码信息
port: 9800
tomcat:
uri-encoding: UTF-8
mybatis:
#配置mapper的指定路径
mapper-locations: classpath*:com/demo/**/dao/*.xml
#防止空值异常报错
configuration:
jdbc-type-for-null: 'null'
spring:
#设置高版本Swagger匹配策略
mvc:
pathmatch:
matching-strategy: ant_path_matcher
#配置数据源
datasource:
name: mydatasource
#自定义数据源
type: com.alibaba.druid.pool.DruidDataSource
druid:
#监控统计拦截的filters
filters: stat
driver-class-name: com.mysql.cj.jdbc.Driver
#连接基本属性
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC
username: root
password: root
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 60000
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
#用来验证连接是否有效的sql
validation-query: SELECT 'x'
#申请连接时检测空闲时间
test-while-idle: true
#从连接池获取连接时是否检查连接有效性,true检查,false不检查
test-on-borrow: false
#归还连接时是否检查连接有效性,true检查,false不检查
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
#连接池中最大的预处理连接数量
max-pool-prepared-statement-per-connection-size: 20
stat-view-servlet:
enabled: false
redis:
host: localhost
port: 6379
logging:
level:
# 给指定的包设置日志级别
com.demo.user: DEBUG
#Swagger2是否可用
swagger2:
enable: true
User表SQL
用于快速建user表
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `user`
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(20) NOT NULL,
`name` varchar(20) DEFAULT NULL,
`pw` varchar(20) DEFAULT NULL,
`role` varchar(20) DEFAULT NULL COMMENT '用户角色',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1001', 'zs', '12345', 'user');
INSERT INTO `user` VALUES ('1002', 'ls', '1234', 'admin');
INSERT INTO `user` VALUES ('1004', 'ww', 'qwer', null);
INSERT INTO `user` VALUES ('1005', 'zl', '1234457', null);
INSERT INTO `user` VALUES ('1008', '赵六', 'qwer', null);
INSERT INTO `user` VALUES ('1009', '李四', '12345', null);
INSERT INTO `user` VALUES ('1010', '123', '123', null);
INSERT INTO `user` VALUES ('1011', '1', '1', null);
操作演示
正常流程
登录

登录成功会转到用户首页

登陆后的操作
这里以点击一个按钮为演示

表明带token的请求不再被拦截,可以正常执行。
退出登录按钮
点击之后会转到登录页。即使点击浏览器的后退按钮,再操作界面,也需要再次登录。
同时会清除前端本地存储的用户信息,以及redis上的用户信息。

登录失败演示
当输入错误的账号或者密码时:

部分异常流程
未登录时访问其他界面
浏览器地址栏直接输入用户首页,回车访问

会转到登录页 让用户登录
不过这里 要保证token为空或者是错误的

token过期或者错误点击按钮转到登录页重新登录
修改本地存储的token,点击按钮

会直接转到登录页

过期token发起的的请求返回

遇到的问题
在弄这个的时候遇到的问题还是蛮多的。
未登录时访问其他界面自动转到登录页
在这里卡了一会,后面想到直接由前端通过token判断拦截,后端不对前端界面做拦截。
登陆失败的处理不被调用
认证失败本来是想由指定的认证异常处理的

但是没有生效,最后写到了CustomAuthenticationEntryPoint里面。
资源读取不到但项目目录下却有
有时候你的项目里有资源,但是你却访问不到。大概率是资源导出配置的问题。
直接去target目录下找找有没有,没有的话直接复制进去。

总结
写这一篇大概前前后后写了快两万五千字,中间很多次去Spring官网查相关英文的文档,csdn上的大部分是低版本的Spring Security配置,用高版本的还要另外找资料,这些花了很多的时间。
还有就是前端界面,本来想用Thymeleaf的,后面又觉得又不符合前后端分离的架构,再加上实在不熟,就又自己去找了资料学了下前端的东西,写了点前端的界面。
删删减减,涂涂改改终于是结束了。今晚看球!



















