从头开始搭建一个SpringBoot项目--SpringSecurity的配置
创始人
2024-04-20 04:30:49
0

从头开始搭建一个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身份认证流程分析

在后文中的相关部分中,你将还会看到上述文章的引用

本文的目标

阅读本文你将学会以下内容:

  1. 使用Spring Security进行身份验证
  2. token验证在Spring Security中的使用
  3. Spring Security自定义异常处理
  4. Spring Security自定义过滤器

使用到的依赖、Redis配置、通用返回实体类

依赖

这里的SpringBoot版本的信息在我之前的文章里: 从头开始搭建一个SpringBoot项目-整合MyBatis有说过,SpringBoot的版本是2.7.4

org.springframework.bootspring-boot-starter-securityio.jsonwebtokenjjwt0.9.1
com.alibabafastjson1.2.72org.springframework.bootspring-boot-starter-data-redis2.5.4

Redis

项目里的配置

文章的末尾会又具体的yml文件的信息,这里只是提一下

spring:redis:host: localhostport: 6379

至于使用Redis的原因,大家可以看看我之前的文章: Java生成token的工具类(对称签名)
这里就不再使用Session存储用户信息了,转为使用Redis。安装和在SpringBoot里的配置就不在这里赘述了,大家自行寻找教程配置安装即可。

通用返回实体

在调用接口的时候,大都返回的是code – 状态码、msg – 提示信息、data--数据,那么我们可以给它封装一下。
在这里插入图片描述

Result

接口通用的的返回

@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@ToString
public class Result {//状态码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;}
}

配置文件

配置的目录结构

在这里插入图片描述
DruidConfigSwagger2Config在以下的文章里讲到过,这里就不再赘述了。
从头开始搭建一个SpringBoot项目-整合MyBatis
从头开始搭建一个SpringBoot项目–Swagger2的配置

Spring Security的配置信息

版本中Spring Security的配置文件通常是通过继承一个WebSecurityConfigurerAdapter的父类,来Spring Security文件的配置
如以下这样

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//构建认证管理器的配置}@Overrideprotected void configure(HttpSecurity http) throws Exception {//框架主要的配置}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {//认证管理器的配置}
}

但是在一些高版本的Spring Security已经不再使用上述方法,而是使用@Bean注解的方式来配置,如以下这样

SecurityConfig

Spring Security的配置信息放在这里。以bean的形式配置

@Configuration
public class SecurityConfig {//权限异常的处理 多指访问非自己权限内的信息 本文搭建的并未用到@AutowiredCustomAccessDeniedHandler customAccessDeniedHandler;//认证失败的错误处理 密码错误 token异常等@AutowiredCustomAuthenticationEntryPoint customAuthenticationEntryPoint;//自定义的过滤器@AutowiredJwtFilter jwtFilter;//数据库底层操作@AutowiredSecurityUserServices userServices;//加密器的设置@Beanpublic PasswordEncoder getPasswordEncoder() {return new BCryptPasswordEncoder();}@BeanSecurityFilterChain 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();}@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {// 不需要走过滤链的请求 也就是忽略过滤链 这里忽略druid 不然无法查看return (web) -> web.ignoring().antMatchers("/druid/**" );}//配置跨源访问@BeanCorsConfigurationSource corsConfigurationSource() {UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues());return source;}//认证管理器 将其包装为Bean 用于自定义过滤器JwtFilter中的认证@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}}

WebMVCConfig

web的一些配置

@Configuration
public class WebMVCConfig implements WebMvcConfigurer {//资源映射器@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {//将地址栏输入的/pro映射到/static/proregistry.addResourceHandler("/pro/**").addResourceLocations("classpath:/static/pro/");}//视图控制器@Overridepublic void addViewControllers(ViewControllerRegistry registry) {}//跨域配置@Overridepublic 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;@Overridepublic Collection getAuthorities() {//默认权限为空return null;}@Overridepublic String getPassword() {//数据库存储的不是密文 验证时需要使用密文 所以密码加密在//SecurityUserServices里处理return this.pw;}@Overridepublic String getUsername() {return this.name;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

数据库底层操作

下面是数据库相关的代码及说明

UserDao及UserBeanDao.xml

底层的接口及其配置文件信息

UserBeanDao

public interface UserBeanDao {//根据用户名称获取用户对象UserBean getUserByUserName(String name);//框架的的认证方式 - 这个UserBean loadUserByUsername(String name);
}

UserBeanDao.xml






自己的业务逻辑层

用户的业务逻辑处理接口及实现类: 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的认证管理器 用于认证@Autowiredprivate AuthenticationManager authenticationManager;//redis的工具类 用于认证成功后向redis中存入登录信息@AutowiredRedisUtil redisUtil;//数据库底层操作@AutowiredUserBeanDao dao;@Overridepublic UserBean getUserByUserName(String name) {return dao.getUserByUserName(name);}//登录接口 使用Spring Security的登录逻辑@Overridepublic Result login(String userName, String password) {//由Spring Security认证管理器实现认证Authentication authentication = authenticationManager.authenticate(//将用户名和密码 包装成一个UsernamePasswordAuthenticationToken 用于认证UsernamePasswordAuthenticationToken.unauthenticated(userName , password));//将得到认证信息强转为用户实体UserBean userBean = (UserBean) authentication.getPrincipal();//以登陆的用户名获取tokenString token = TokenUtil.getToken(userBean.getName());//以键值对信息 向redis中存入 用户名 - 用户信息redisUtil.setCacheObject("loginId:"+ userBean.getUsername() , userBean);//将token和用户信息返回给前端HashMap 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提供的认证逻辑的底层@AutowiredUserBeanDao userBeanDao;//实现loadUserByUsername方法@Overridepublic 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 {@AutowiredUserBeanServices userBeanServices;@AutowiredRedisUtil 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);elsereturn 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("退出登录成功");}elsereturn ResultUtil.error(ResultCode.ERROR);}
}

自定义异常

Spring Security中最常见的两中错误就是:
AuthenticationException:认证异常 - 密码错误、身份过期等。在这里由CustomAuthenticationEntryPoint处理

AccessDeniedException:拒绝连接异常 - 常见的是权限不够。在这里由CustomAccessDeniedHandler处理

对于前一种登陆时的错误,我们要返回提示信息,比如:账号或密码错误什么的。对于身份过期,我们要转到登录页,让他重新登陆

第二种错误可以只返回一个提示信息,提醒他权限不够。

CustomAuthenticationEntryPoint

自定义认证异常处理,用于处理登录信息出错身份过期等情况。

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic 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 {@Overridepublic 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中加入一个过滤器来实现自定义的功能。那我们该怎么做?只需要思考以下两个问题:

  1. 如何放入到Spring Security框架的FilterChain(过滤链)中
  2. 放入到FilterChain中的中的什么位置

什么?你不知道Spring Security的FilterChain(过滤链)?

以下内容来自于本人翻译Spring官网里的Spring Security,翻译文章的地址为: SpringSecurity官网的Architecture部分的翻译

Security Filters(安全过滤器)
Security FilterSecurityFilterChainAPI插入到FilterChainProxy中。这些Filter的顺序是很重要的。通常我们并不需要知道SpringSecurityFilter的顺序。但有时候,知道顺序是有帮助的。

以下是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工具类@Autowiredprivate RedisUtil redisUtil;//具体功能在此方法内写@Overrideprotected 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或者登录 走UsernamePasswordAuthenticationFilterfilterChain.doFilter(httpServletRequest, httpServletResponse);return;}//解析tokenString 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 {@Autowiredprivate RedisTemplate redisTemplate;//设置token信息到redis中public  void setCacheObject(final String key, final T value) {redisTemplate.opsForValue().set(key, value);}//根据用户名从redis获取token信息public  T getCacheObject(final String key) {ValueOperations 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 – 错误页



错误信息页

错误信息页


index.html – 用户首页



首页


登陆成功的首页





login.html – 登录页



js示例文件

js说明

这里就不放js代码了,有需要的,自行去官网找资源即可。也可以参考以下文章: 简单引入JQuery

application.yml

最终运行的配置文件

server:#配置端口及编码信息port: 9800tomcat:uri-encoding: UTF-8mybatis:#配置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.DruidDataSourcedruid:#监控统计拦截的filtersfilters: statdriver-class-name: com.mysql.cj.jdbc.Driver#连接基本属性url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTCusername: rootpassword: root#配置初始化大小/最小/最大initial-size: 1min-idle: 1max-active: 20#获取连接等待超时时间max-wait: 60000#间隔多久进行一次检测,检测需要关闭的空闲连接time-between-eviction-runs-millis: 60000#一个连接在池中最小生存的时间min-evictable-idle-time-millis: 300000#用来验证连接是否有效的sqlvalidation-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。分库分表较多推荐设置为falsepool-prepared-statements: false#连接池中最大的预处理连接数量max-pool-prepared-statement-per-connection-size: 20stat-view-servlet:enabled: falseredis:host: localhostport: 6379logging: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的,后面又觉得又不符合前后端分离的架构,再加上实在不熟,就又自己去找了资料学了下前端的东西,写了点前端的界面。
删删减减,涂涂改改终于是结束了。今晚看球!

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...