【SSM项目】尚筹网(五)项目改写:使用前后端分离的SpringSecurityJWT认证
在项目中加入SpringSecurity
1 加入依赖
<!-- SpringSecurity --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring-security-version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>${spring-security-version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring-security-version}</version> </dependency>
2 配置DelegatingFilterProxy代理过滤器
SpringSecurity需要借助一系列的ServletFilter来实现安全性的功能,但是肯定不能我们需要自己去一个个创建这些过滤器,这里使用了一种代理模式
,即创建一个DelegatingFilterProxy的Bean,在工作中会由这个Bean拦截发往应用的所有请求,并将请求委托给id为springSecurityFilterChain的bean进行SpringSecurity安全性处理。
2.1 使用web.xml的方式
这里最重要的是<fileter-name>必须设置为
springSecurityFilterChain
,因为我们将SpringSecurity配置在Web安全性之中会有一个名为springSecurityFilterChain的FilterBean,DelegatingFilterProxy就会将过滤逻辑委托给它进行创建。
2.2 使用Java的方式
这里我们是基于Servlet3.0搭建的项目,所以选用的Java的方式配置DelegatingFilterProxy,我们只需要创建一个继承AbstractSecurityWebApplicationInitializer
的实现类即可,这个实现类由于实现了WebApplicationInitializer
接口,因此会和我们的web容器一样会在启动的时候被Spring发现,然后会自动将配置好的DelegatingFilterProxy
注入Web容器。
public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer { }
3 创建SpringSecurity配置类
@Configuration @EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); } }
4 ☆谁来扫描我们配置的SpringSecurity配置类?
4.1 基于xml的方式
出现这个问题是由于我们当前的项目是有两个IOC容器的,一个是在AppConfig中配置的SpringMVC(扫描controller)另一个是在RootConfig中配置的Spring容器(扫描除了Controller的组件,如整合Mybatis,持久化等方面的组件)。而SpringSecurity是WEB层面的安全检查,主要应针对发送给应用的请求所以应该由MVC容器进行扫描。
但是衍生出来的问题就是SpringSecurity配置的DelegatingFilterProxy
会在被扫描的IOC容器中寻找之前提到的springSecurityFilterChain
的Bean,这样就会出现下面的问题:
问题分析:在web容器启动的源码中,默认就是去找父容器Spring容器

解决方法1:不使用ContextLoaderListener
,让DispatcherServlet加载所有的IOC容器。
DelegatingFilterProxy在初始化的时候查找IOC容器,找不到 放弃
第一次请求的时候再查找,找到SpringMVC的IOC容器,能够找到所需要的bean
但是这种方法会破坏原有程序的结构:ContextLoaderListener和DispatcherServlet各创建一个IOC容器。
解决方法2:修改源码(算了算了
4.2 基于Java的配置方式
基于Servlet3.0的方式搭建的项目则不会出现这个问题,原因是我们使用的是AbstractAnnotationConfigDispatcherServletInitializer
代替的web.xml并且这个实现类会自动创建DispatcherServlet
,随后通过@Configuration配置类的配置最后会合成到一个,都属于上面的DispatcherServlet创建的IOC容器,所以无论被那个配置类扫描到都能被所有的配置类所使用。
5 SpringSecurity工作原理
在初始化或者第一次请求时准备好过滤器链,具体任务由具体过滤器来实现。
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter org.springframework.security.web.context.SecurityContextPersistenceFilter org.springframework.security.web.header.HeaderWriterFilter org.springframework.security.web.csrf.CsrfFilter org.springframework.security.web.authentication.logout.LogoutFilter org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter org.springframework.security.web.authentication.www.BasicAuthenticationFilter org.springframework.security.web.savedrequest.RequestCacheAwareFilter org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter org.springframework.security.web.authentication.AnonymousAuthenticationFilter org.springframework.security.web.session.SessionManagementFilter org.springframework.security.web.access.ExceptionTranslationFilter org.springframework.security.web.access.intercept.FilterSecurityInterceptor
前后端分离的登录验证
1 放行登录和静态请求
这里我们使用的前后端分离的做法,这时候前端登录会显示403没有权限,所以只需要在SpringSecurity中放行登录请求就可以了。
@Configuration @EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { // 登录白名单 private final String[] URL_WHITELIST = new String[] { "/login", "/logout", "/test/**" }; @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity security) throws Exception { security .cors() // 开启跨域 .and() .csrf() // 关闭csrf .disable() .formLogin() //.successHandler() //.failureHandler() .and() .logout() //.logoutSuccessHandler() .and() // 禁用session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(URL_WHITELIST) // 登录白名单 放行 .permitAll() .and() .authorizeRequests() .anyRequest() .authenticated(); // 异常处理配置 // 自定义配置 } }
这里注意要禁用CSRF,否则提交的表单必须携带之前提到的_csrf.parameterName,如下,否则仍然会报403错误
<form action="${pageContext.request.contextPath}/login" method="post"> <%-- <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>--%> <div class="layadmin-user-login-main">
2 重写并配置登录成功和失败处理器
/** * 登录成功处理器 */ @Component public class LoginSuccessHandle implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 设置响应体格式为json response.setContentType("application/json;application/json;charset=UTF-8"); String userName = "userName"; String token = JWTUtil.getJWTToken(userName); // 将token封装为返回体 Map<String, String> map = new HashMap<>(); map.put("token", token); R<Object> r = R.successWithData(map); // 转换为json Gson gson = new Gson(); String json = gson.toJson(r); response.getWriter().write(json); } }
/** * 登录失败处理器 */ @Component @Slf4j public class LoginFailureHandle implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); String message = e.getMessage(); if(e instanceof BadCredentialsException) { message = "用户名或者密码错误捏"; } R<Object> r = R.failed(message); Gson gson = new Gson(); String json = gson.toJson(r); response.getWriter().write(json); } }
在SpringSecurity中配置登录成功和失败处理器
@Override protected void configure(HttpSecurity security) throws Exception { security .cors() // 开启跨域 .and() .csrf() // 关闭csrf .disable() .formLogin() // 表单登录 .usernameParameter("loginName") .passwordParameter("passWord") .successHandler(loginSuccessHandle) .failureHandler(loginFailureHandle) //.and() //.logout() //.logoutSuccessHandler() .and() // 禁用session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(URL_WHITELIST) // 登录白名单 放行 .permitAll() .and() .authorizeRequests() .anyRequest() .authenticated(); // 异常处理配置 // 自定义配置 }
paramter用于设置参数,不设置的话默认传来的数据为username 和 password
3 一个比较坑的点:SpringSecurity的默认登录验证不支持json问题
当登录接口为下面的情况时,前端是以json向后端发送登录数据,然后会经过登陆失败处理器返回登录失败:
export function login(data) { return request({ url: '/login', method: 'post', data }) }
当修改为get请求时,便能够正常登录由登录成功处理器处理响应了。
export function login(data) { return request({ url: '/login', method: 'post', params: data }) }
axios中post请求使用params参数会等同于get请求,如上面的请求接口就是
[http://localhost:9528/api/login?loginName=admin&passWord=111111]
当然也可以直接以?加参数的形式以get请求发送,效果是一样的
这种方式存在的问题是登录名和密码会直接显示在url里面,隐蔽性不好(隐蔽性主要是指的可以在浏览记录中检索,post请求携带的数据只是不会被检索到而已,至于安全性整个http都是不安全的)
使用json方式进行登录验证可以参考知乎的一篇文章这里先不写了,等后面有时间改进一下:[https://zhuanlan.zhihu.com/p/365515428](Spring Security 使用自带的 formLogin)
4 基于SpringSecurity的登录查库
上面的例子是在内存中进行登录验证的,接下来实现数据库验证登录信息。
4.1 编写UserDetailsService实现类
@Slf4j @Component public class MyUserDetailService implements UserDetailsService { @Autowired AdminService adminService; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { // 数据库查询 Admin admin = adminService.getByLoginName(s); if(admin == null) { throw new UsernameNotFoundException(""); } if("1".equals(admin.getStatus())) { throw new AdminCountLockException("账号被禁用,请联系管理员解封"); } String passWord = admin.getPassWord(); String userName = admin.getUserName(); return new User(userName, passWord, getAdminAuthority()); } private List<GrantedAuthority> getAdminAuthority() { return new ArrayList<>(); } }
4.2 开启SpringSecurity数据库认证
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 内存验证 //auth.inMemoryAuthentication() // .withUser("admin") // .password("111111") // .roles("admin"); auth.userDetailsService(userDetailsService); }
4.3 开启带盐值加密的BCryptPasswordEncoder
@Configuration @Import(BCryptPasswordEncoder.class) @EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LoginSuccessHandle loginSuccessHandle; @Autowired private LoginFailureHandle loginFailureHandle; @Autowired private UserDetailsService userDetailsService; @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; // 登录白名单 private final String[] URL_WHITELIST = new String[] { "/login", "/logout", "/test/**" }; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 内存验证 //auth.inMemoryAuthentication() // .withUser("admin") // .password("111111") // .roles("admin"); auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); }
5 实现JWT认证过滤器
目的:实现登录之后在不使用session的情况下对登录状态进行检验。
5.1 创建一个JWT自定义配置类
JWTAuthorityFilter
@Slf4j public class JWTAuthorityFilter extends BasicAuthenticationFilter { @Autowired AdminService adminService; @Autowired MyUserDetailService userDetailService; // 验证白名单 private final String[] URL_WHITELIST = new String[] { "/login", "/logout", "/test/**" }; public JWTAuthorityFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = request.getHeader("token"); String uri = request.getRequestURI(); log.info("请求URI:" + uri); // 放行白名单 if(Arrays.asList(URL_WHITELIST).contains(uri)) { chain.doFilter(request, response); } // token验证 CheckResult checkResult = JWTUtil.validateJwt(token); if(!checkResult.isSuccess()) { int errorCode = checkResult.getErrorCode(); if(JWTConstant.JWT_ERROR_CODE_NULL == errorCode) { throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_NULL); } else if(JWTConstant.JWT_ERROR_CODE_FAIL == errorCode) { throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_FAIL); } else { throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_EXPIRE); } } // 将用户认证信息放入SpringSecurity上下文 Claims claims = JWTUtil.parseJWT(token); String userName = claims.getSubject(); Admin admin = adminService.getByLoginName(userName); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, null, userDetailService.getAdminAuthority()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); chain.doFilter(request, response); } }
过滤器的主要工作有:
获取请求头中的token进行验证,验证不通过则抛出JwtException。
放行验证白名单给后面的Filter
将用户认证信息放入SpringSecurity上下文
5.2 在SpringSecurity配置类中设置自定义配置过滤器
@Bean JWTAuthorityFilter jwtAuthorityFilter() throws Exception { JWTAuthorityFilter jwtAuthorityFilter = new JWTAuthorityFilter(authenticationManager()); return jwtAuthorityFilter; }
.and() .addFilter(jwtAuthorityFilter());
5.3 配置JWT异常处理
① 编写AuthenticationEntryPoint
实现类
/** * JWT自定义认证失败处理 */ @Slf4j @Component public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); String message = e.getMessage(); log.error(message); R<Object> r = R.failed(message); Gson gson = new Gson(); String json = gson.toJson(r); response.getWriter().write(json); } }
② 在SpringSecurity中配置JWT异常处理
@Override protected void configure(HttpSecurity security) throws Exception { security .cors() // 开启跨域 .and() .csrf() // 关闭csrf .disable() .formLogin() .usernameParameter("loginName") .passwordParameter("passWord") .successHandler(loginSuccessHandle) .failureHandler(loginFailureHandle) //.and() //.logout() //.logoutSuccessHandler() .and() // 禁用session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(URL_WHITELIST) // 登录白名单 放行 .permitAll() .and() .authorizeRequests() .anyRequest() .authenticated() // 异常JWT异常处理配置 .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 自定义配置JWT认证过滤器 .and() .addFilter(jwtAuthorityFilter()); }
5.4 代码总结 + JWT异常配置无法生效
SpringSecurity配置类
@Configuration @Import(BCryptPasswordEncoder.class) @EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private LoginSuccessHandle loginSuccessHandle; @Autowired private LoginFailureHandle loginFailureHandle; @Autowired private UserDetailsService userDetailsService; @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Bean JWTAuthorityFilter jwtAuthorityFilter() throws Exception { return new JWTAuthorityFilter(authenticationManager()); } // 登录白名单 private final String[] URL_WHITELIST = new String[] { "/login", "/logout", "/test/**" }; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 内存验证 //auth.inMemoryAuthentication() // .withUser("admin") // .password("111111") // .roles("admin"); auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder); } @Override protected void configure(HttpSecurity security) throws Exception { security .cors() // 开启跨域 .and() .csrf() // 关闭csrf .disable() .formLogin() .usernameParameter("loginName") .passwordParameter("passWord") .successHandler(loginSuccessHandle) .failureHandler(loginFailureHandle) //.and() //.logout() //.logoutSuccessHandler() .and() // 禁用session .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(URL_WHITELIST) // 登录白名单 放行 .permitAll() .and() .authorizeRequests() .anyRequest() .authenticated() // 异常JWT异常处理配置 .and() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) // 自定义配置JWT认证过滤器 .and() .addFilter(jwtAuthorityFilter()); } }
JWT认证过滤器
@Slf4j public class JWTAuthorityFilter extends BasicAuthenticationFilter { @Autowired AdminService adminService; @Autowired MyUserDetailService userDetailService; // 验证白名单 private final String[] URL_WHITELIST = new String[] { "/login", "/logout", "/test/**" }; public JWTAuthorityFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String token = request.getHeader("token"); String uri = request.getRequestURI(); log.info("请求URI:" + uri); // 放行白名单 if(Arrays.asList(URL_WHITELIST).contains(uri)) { chain.doFilter(request, response); } // token验证 CheckResult checkResult = JWTUtil.validateJwt(token); if(!checkResult.isSuccess()) { int errorCode = checkResult.getErrorCode(); log.error(errorCode+""); String message = JWTConstant.JWT_ERROR_MESSAGE_EXPIRE;; if(JWTConstant.JWT_ERROR_CODE_NULL == errorCode) { message = JWTConstant.JWT_ERROR_MESSAGE_NULL; //throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_NULL); } else if(JWTConstant.JWT_ERROR_CODE_FAIL == errorCode) { message = JWTConstant.JWT_ERROR_MESSAGE_FAIL; //throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_FAIL); } else { message = JWTConstant.JWT_ERROR_MESSAGE_EXPIRE; //throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_EXPIRE); } response.setContentType("application/json;charset=UTF-8"); R<Object> r = new R<>(errorCode, message, null); Gson gson = new Gson(); String json = gson.toJson(r); response.getWriter().write(json); return ; } // 将用户认证信息放入SpringSecurity上下文 Claims claims = JWTUtil.parseJWT(token); String userName = claims.getSubject(); Admin admin = adminService.getByLoginName(userName); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, null, userDetailService.getAdminAuthority()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); chain.doFilter(request, response); } }
JWT认证失败处理
/** * JWT自定义认证失败处理 */ @Slf4j @Component public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); String message = e.getMessage(); log.error(message); R<Object> r = R.failed(message); Gson gson = new Gson(); String json = gson.toJson(r); response.getWriter().write(json); } }
其实上面之前的异常处理代码是根本生效不了的,搞了一晚上,问题是出现在JWT认证过滤器这里:
if(!checkResult.isSuccess()) { int errorCode = checkResult.getErrorCode(); log.error(errorCode+""); String message = JWTConstant.JWT_ERROR_MESSAGE_EXPIRE;; if(JWTConstant.JWT_ERROR_CODE_NULL == errorCode) { message = JWTConstant.JWT_ERROR_MESSAGE_NULL; //throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_NULL); } else if(JWTConstant.JWT_ERROR_CODE_FAIL == errorCode) { message = JWTConstant.JWT_ERROR_MESSAGE_FAIL; //throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_FAIL); } else { message = JWTConstant.JWT_ERROR_MESSAGE_EXPIRE; //throw new JwtException(JWTConstant.JWT_ERROR_MESSAGE_EXPIRE); } response.setContentType("application/json;charset=UTF-8"); R<Object> r = new R<>(errorCode, message, null); Gson gson = new Gson(); String json = gson.toJson(r); response.getWriter().write(json); return ; }
这里注释掉的内容就是之前的代码,问题就是
①每次认证失败就会直接抛出异常到前端而不是异常处理器,也就是过滤器链根本执行不完,也就不可能这种情况到达异常处理器。
②然后我又尝试使用一个拦截器拦截抛出的异常然后包装返回,但是也是生效不了(机制还是不清楚。。需要多去看书)
代码应该是没有错误,但是应该是因为我使用的是Servlet3.0配置的web容器,然后导入的JWT而不是视频的SpringBoot + 各种场景启动器
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步