从头开始搭建一个SpringBoot项目--SpringSecurity的配置

news2025/3/1 21:47:18

从头开始搭建一个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

<!--		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;
    }
}

配置文件

配置的目录结构

在这里插入图片描述
DruidConfigSwagger2Config在以下的文章里讲到过,这里就不再赘述了。
从头开始搭建一个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中加入一个过滤器来实现自定义的功能。那我们该怎么做?只需要思考以下两个问题:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/75106.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入浅出自定义创建spring-boot-starter

深入浅出自定义创建spring-boot-starter https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration 快速入手 第一步&#xff1a;新建模块 第二步&#xff1a;修改依赖 <?xml version"1.0" e…

关键词(三)

关键词一.最冤枉的关键字—sizeof二.“简单”的布尔类型—_Bool一.最冤枉的关键字—sizeof 前面我们说过定义变量是需要空间的&#xff08;声明不需要&#xff09;&#xff0c;同时你需要有类型像int,char…这些不同的类型会在内存中开辟不同大小的空间&#xff0c;而sizeof就可…

Web安全测试工具AppScan简述

01 安全测试的对象 了解常见的Web应用安全漏洞&#xff0c;参考OWASP Top 10 2017。 理解这些常见漏洞的攻击原理&#xff0c;如何判断系统是否存在这些漏洞、如何防止这些漏洞。 02 安全测试的实施 SQL注入测试 确认所有的解释器都明确的将不可信数据从命令语句或者查询语…

微服务囧途之BFF层登场

从单体架构演化为微服务架构后&#xff0c;架构者的期望是“模块A”“模块B” “后端服务”。 场景一 Web端和Mobile端都有一个详情页面&#xff0c;需要调用模块A的getDetail接口获取数据。假设Web端实际需要展示的字段是20个&#xff0c;Mobile端实际需要展示的字段是10个…

记一次Metrics-server异常

报错 前几天测试环境k8s集群做etcd的备份恢复。 所有的pod都起来了&#xff0c;包括metrics-server的状态也是 Running,部署新pod也没有异常&#xff0c;结果kubectl top 请求的时候报错了 Error from server (ServiceUnavailable): the server is currently unable to handle …

Spring cloud Consul 服务注册中心实战

Spring cloud Consul服务注册中心一、简介二、功能三、角色四、工作原理1、服务注册及发现2、服务调用五、起步1、下载2、安装六、实例一、简介 Consul 用于实现分布式系统的服务注册与配置&#xff0c;与其它方案相比&#xff0c;Consul 更 “一站式”&#xff0c;内置了服务…

移植lighttpd笔记

一、前言 lighttpd交叉编译并移植到iTop4412平台 二、编译环境 ubuntu 18.04pcrelighttpd交叉编译链:arm-none-linux-gnueabi三、编译过程 1.pcre交叉编译 tar -xvf pcre-8.38.tar.bz2 cd pcre-8.38/ ./configure --host=arm-none-linux-gnueabi --prefix=/ --enable-utf8 …

IDEA中如何实现 git stash 命令的可视化操作?

目录 问题现象&#xff1a; 问题分析&#xff1a; 拓展&#xff1a;git stash 相关命令 1、git stash 2、git stash save 注释 3、git stash list 4、git stash pop 5、git stash apply stash{编号} 6、git stash drop stash{编号} 7、git stash clear 8、git stash…

flutter 内网安装包生成二维码

参考 前言 有时候我们可能需要用手机访问电脑上的文件, 或者用手机测试电脑上编写的程序 此时 在同一个wifi网络的前提下我们可以这么做: 第一种 使用python 直接使用自带python工具开启http服务, 首先打开终端, 进入需要共享的目录,然后运行以下代码即可: python -m Simple…

React TreeSelect 组件使用和优化

1、自定义折叠和展开图标 此时就要用到switcherIcon属性&#xff0c;以下是antd中对switcherIcon属性的描述&#xff0c; switcherIcon自定义树节点的展开/折叠图标ReactNode | ((props: AntTreeNodeProps) > ReactNode)-具体使用如下&#xff1a; import { DownOutlined…

数据库原理及MySQL应用 | 事件

事件由一个特定的线程——事件调度器来管理&#xff0c;事件是根据指定时间表&#xff0c;在某一特定的时间点&#xff0c;触发相关的SQL语句或存储过程。 01、事件概述 事件(Event)是根据指定时间表执行的任务&#xff0c;是MySQL在相应的时刻调用的过程式数据库对象。它由事…

抓包分析ssh远程主机为何变慢了?

文章目录背景SSH协议握手过程ssh 抓包MAC层包传输tcp握手抓包解释三次握手的第一个报文- SYN包第一个报文对应的抓包详情三次握手的第二个报文- SYNACK包第二个报文对应的抓包详情三次握手的第三个报文- ACK包第三个报文对应的抓包详情ssh版本协议交换密钥协商key阶段Key Excha…

程序员的工资这么高,为什么还会有人离职?

出了社会以后才发现&#xff0c;班级里天天打鸡血的、最奋斗的、同时也最焦虑的&#xff0c;不是成绩最好的&#xff0c;也不是成绩最差的&#xff0c;而是那帮处于中间的人。 他们不像那些成绩最差的&#xff0c;或是天天摆烂&#xff0c;或是靠高情商混得风生水起&#xff1b…

Pytest----pluggy源码解读基础准备

【原文链接】Pytest----pluggy源码解读基础准备 解读pluggy源码&#xff0c;直接使用pytest环境中安装的pluggy即可&#xff0c;比如这里安装的pluggy版本是1.0.0&#xff0c;为了更好的理解源码&#xff0c;这里首先使用如下应用代码作为应用实例&#xff0c;从如下代码中可以…

低代码助力生产管理:离散型制造业MES系统

制造业作为我国国民经济的支柱产业&#xff0c;在我国经济增长中占有主导作用。而制造业对经济增长的贡献很大一部分来自于以离散制造业为代表的机械装备制造、汽车零部件制造等。因此&#xff0c;离散制造业的发展对我国经济增长具有举足轻重的作用。 离散型制造业的特点&…

中创股份在科创板提交上会稿:计划募资6亿元,景新海为董事长

12月8日&#xff0c;山东中创软件商用中间件股份有限公司&#xff08;下称“中创股份”&#xff09;在上海证券交易所科创板提交招股书&#xff08;上会稿&#xff09;。相较于此前招股书&#xff0c;中创股份补充了截至2022年9月30日的财务数据等信息。 据贝多财经了解&#x…

记一次 Eclipse 打包的辛酸历程

文章目录1&#xff1a;背景2 maven 工程3 普通工程3.1 打可执行的 jar3.2 打普通 jar4&#xff1a; 运行 jar 包1&#xff1a;背景 偶然的境况下&#xff0c;被迫使用了 Eclipse 进行代码。遇到的代码也有点奇怪&#xff0c;main 方法启动 java 工程&#xff0c;里面封装 Tomc…

音频声音信号

音频信号是模拟信号&#xff0c;我们需要将其保存为数字信号&#xff0c;才能对语音进行算法操作&#xff0c;WAV是Microsoft开发的一种声音文件格式&#xff0c;通常被用来保存未压缩的声音数据。 通道数&#xff1a;同时有个几个设备在进行音频的采样&#xff1b;采样频率&a…

Django连接MySQL与正反向迁移命令

目录 连接MySQL 方法一&#xff1a;pymysql连接 方法二&#xff1a;mysqlclient 迁移命令 连接MySQL 方法一&#xff1a;pymysql连接 第一步&#xff1a;修改settings.py配置文件中的DATABASES&#xff1a; DATABASES {default: {ENGINE: django.db.backends.mysql,HOS…

【车载开发系列】UDS诊断---动态定义DID($0x2C)

【车载开发系列】UDS诊断—动态定义DID&#xff08;$0x2C&#xff09; UDS诊断---动态定义DID&#xff08;$0x2C&#xff09;【车载开发系列】UDS诊断---动态定义DID&#xff08;$0x2C&#xff09;一.概念定义1&#xff09;DID定义方式2&#xff09;DID失效条件二.应用场景三.报…