从头开始搭建一个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
的,后面又觉得又不符合前后端分离的架构
,再加上实在不熟,就又自己去找了资料学了下前端的东西,写了点前端的界面。
删删减减,涂涂改改终于是结束了。今晚看球!