第一节 Spring Security 简介

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)用户授权(Authorization)两个部分。

  • 用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

  • 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

第二节 Spring Security 核心组件

1. Authentication 认证

//这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken//我们自定义实现就可以叫MobileCodeAuthenticationTokenpublic interface Authentication extends Principal, Serializable {​/** * 权限列表 */Collectionextends GrantedAuthority> getAuthorities();​/** * 认证凭据,可能是密码,也可能是验证码,也可能是其他认证凭据 */Object getCredentials();​/** * 认证请求的详细信息,可能是IP地址,也可能是认证序列号,也可能是null */Object getDetails();​/** * 如果通过认证,则返回的是包含(用户名和密码)或者(手机号和验证码)等的对象;如果认证不通过, * 则返回的是用户名或者手机号等。 */Object getPrincipal();​/** * 是否认证通过 */boolean isAuthenticated();​/** * 修改认证状态 */void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}

Authentication接口就是用来携带认证信息的。认证信息包括用户身份信息,密码,及权限列表等

2. UsernamePasswordAuthenticationFilter

账号密码认证过滤器,用于认证用户信息,认证方式是由 AuthenticationManager 接口提供。

3. AuthenticationManager

public interface AuthenticationManager {​/*** 尝试认证传递过来的认证信息,如果认证成功,则会修改认证信息的状态。否则,则会抛出异常*/Authentication authenticate(Authentication authentication) throws AuthenticationException;​}

认证管理器,是认证相关的核心接口,也是发起认证的出发点。实际业务中可能根据不同的信息进行认证,所以Spring推荐通过实现 AuthenticationManager 接口来自定义自己的认证方式。Spring 提供了一个默认的实现 ProviderManager。

4. ProviderManager

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {//省略其他属性private List<AuthenticationProvider> providers = Collections.emptyList();//省略其他内容}

认证提供者管理器,该类中维护了一个认证提供者列表,只要这个列表中的任何一个认证提供者提供的认证方式认证通过,认证就结束。

5. AuthenticationProvider

public interface AuthenticationProvider {​/** * 执行认证,并返回认证结果 */Authentication authenticate(Authentication authentication) throws AuthenticationException;​/** * 支持认证的类型,用于实现自定义认证,比如手机号和短信登录认证需要用户自己来实现 */boolean supports(Class authentication);}

认证提供者,这是一个接口,具体如何认证,就看如何实现该接口。Spring Security 提供了 DaoAuthenticationProvider 实现该接口,这个类就是使用数据库中数据进行认证。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {//省略其他属性private PasswordEncoder passwordEncoder; //密码加密器,主要用于密码加密private UserDetailsService userDetailsService; //用户详细信息服务,主要用于查询认证用户信息//省略其他内容}

6. UserDetailsService

public interface UserDetailsService {​/*** 根据用户名获取用户详细信息*/UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;}

7. UserDetails

public interface UserDetails extends Serializable {​/** * 用户拥有的权限列表 */Collection<? extends GrantedAuthority> getAuthorities();​/** * 密码 */String getPassword();​/** * 账号 */String getUsername();​/** * 账号是否过期,过期的账号不能进行认证 */boolean isAccountNonExpired();​/** * 账号是否被锁定 */boolean isAccountNonLocked();​/** * 凭据是否过期,过期的凭据不能进行认证 */boolean isCredentialsNonExpired();​/** * 账号是否被启用,未启用的账号不能进行认证 */boolean isEnabled();}

用户的详细信息,主要用于登录认证。

8. SecurityContextHolder

SecurityContextHolder 是最基本的对象,它负责存储当前 SecurityContext 信息。SecurityContextHolder默认使用 ThreadLocal 来存储认证信息,意味着这是一种与线程绑定的策略。在Web场景下的使用Spring Security,在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。

9. SecurityContext

SecurityContext 负责存储认证通过的用户信息(Authentication对象),保存着当前用户是什么,是否已经通过认证,拥有哪些权限等等。

10. AuthenticationSuccessHandler

AuthenticationSuccessHandler 主要用于认证成功后的处理,比如返回页面或者数据。

11. AuthenticationFailureHandler

AuthenticationFailureHandler 主要用于认证失败后的处理,比如返回页面或者数据。

12. AccessDecisionManager

AccessDecisionManager 主要用于实现权限,决定请求是否具有访问的权限。

13. AccessDeniedHandler

AccessDeniedHandler 主要用于无权访问时的处理

第三节 Spring Security 工作流程

1. DelegatingFilterProxy

public class DelegatingFilterProxy extends GenericFilterBean {}​public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {}

由上面的类定义可以看出,DelegatingFilterProxy是一个 Filter,同时,也是一个InitializingBean

public interface InitializingBean {​/** * 当Bean的所有属性设置后由包含的BeanFactory调用,而BeanFactory就是用来创建bean的,换言之,就是将Bean纳入Spring IOC容器 */void afterPropertiesSet() throws Exception;}

实现了InitializingBean接口的类,在创建对象并完成属性设置后,会被纳入Spring IOC 容器管理。如果DelegatingFilterProxyweb.xml中配置,那么,在容器启动时就会实例化该Filter,然后完成初始化,随后被纳入 Spring IOC 容器管理。这样就相当于与 Spring 完成整合。

DelegatingFilterProxy 由 spring web 提供,与 Spring Security 无关。那么 DelegatingFilterProxy 到底有什么作用呢?

其作用是代理真正的Filter实现类

DelegatingFilterProxy 如何知道其所代理的Filter是哪个呢?

这是通过其自身的targetBeanName的属性来确定的,通过该名称,DelegatingFilterProxy可以从WebApplicationContext中获取指定的 bean 作为代理对象。该属性可以通过在web.xml 中定义 DelegatingFilterProxy 时通过 init-param 来指定,如果未指定,则将取其在web.xml中声明时定义的名称作为 targetBeanName 的值

<filter><filter-name>springSecurityFilterChain</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class><init-param> <param-name>targetBeanName</param-name><param-value>springSecurityFilterChain</param-value></init-param></filter><filter-mapping><filter-name>springSecurityFilterChain</filter-name><url-pattern>/*</url-pattern></filter-mapping>

2. FilterChainProxy

使用Spring Security时,DelegatingFilterProxy 代理的就是一个 FilterChainProxy。当我们使用基于Spring Security的NameSpace进行配置时,系统会自动为我们注册一个名为 springSecurityFilterChain 类型为 FilterChainProxy 的 Bean,这也是为什么我们在使用 Spring Security 时需要在 web.xml 中声明一个 name 为 springSecurityFilterChain 的 DelegatingFilterProxy 的 Filter 了。

FilterChainProxy 有什么作用呢?

一个 FilterChainProxy 中可以包含有多个 FilterChain,但是某个请求只会对应一个 FilterChain,而一个 FilterChain 中又可以包含有多个 Filter。

而 Spring Security 底层正是通过一系列的 Filter 来工作的。具体详情如下:

2.1 WebAsyncManagerIntegrationFilter

将Security上下文与SpringWeb中用于处理异步请求映射的WebAsyncmanager进行集成

2.2 SecurityContextPersistenceFilter

在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后麻将SecurityContextHolder中关于这次请求的信息存储的‘仓储’中,然后将SecurityContextHolder中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的

2.3 HeaderWriterFilter

用于将头信息加入响应中

2.4 CsrfFilter

用于处理跨站请求伪造

2.5 LogoutFilter

用于处理退出登录

2.6 UsernamePasswordAuthenticationFilter

[重点]

用于处理基于表单的登录请求,从表单中获取用户名和密码,默认情况下处理来自/login的请求,从表单中获取用户名和密码, 默认使用表单name值为username和password,这两个值可以通过这个过滤器的usernaemparamter个passwordParameter连个参数的值进行修改

2.7 DefaultLoginPageGeneratingFilter

如果没有配置登陆页面,那系统初始化就会配置这个过滤器。并且用于在需要进行登陆时生成一个登陆表单页

2.8 BasicAuthenticationFilter

检测和处理http basic认证

2.9 RequestCacheAwareFilter

用于处理请求的缓存

2.10 SecurityContextHolderAwareRequestFilter

主要包装请求对象request

2.11 AnonymousAuthenticationFilter

检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication

2.12 SessionManagementFilter

管理session的过滤器

2.13 ExceptionTranslationFilter

处理AccessDeniedException和AuthenticationException异常

2.14 FilterSecurityInterceptor

可以看作过滤器链的出口

2.15 RememberMeAuthenticationFilter

当用户没有登录而直接访问资源时,从cookie中找出用户的信息,如果SpringSecurity能够识别出用户提供remember me cookie ,用户将不必填写用户名和密码,而是直接登录进入系统,该过滤器默认从不开启

第三节 Spring Security 认证

1. 数据库认证

1.1 配置认证管理器

package com.qf.authentication.config;​import org.springframework.context.annotation.Bean;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;​@EnableWebSecurity //启用securitypublic class SecurityConfig extends WebSecurityConfigurerAdapter {​//创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder(); }​//认证管理器配置@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//数据库数据认证提供器DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();//设置认证使用的用户详情服务,业就是查询用户信息的服务daoAuthenticationProvider.setUserDetailsService();//设置密码使用的加密器daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());//设置认证管理构建器使用的认证提供器auth.authenticationProvider(daoAuthenticationProvider); }}

1.2 创建查询用户信息服务

package com.qf.authentication.service;​import org.springframework.security.core.userdetails.UserDetailsService;​//用户业务层,继承了UserDetailsService,方便与security结合public interface UserService extends UserDetailsService {}​​package com.qf.authentication.service.impl;​import com.qf.authentication.service.UserService;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;​@Servicepublic class UserServiceImpl implements UserService {​@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return null; }}

1.4 创建用户实体

package com.qf.authentication.model;​import lombok.Data;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;​import java.util.List;​@Datapublic class User implements UserDetails {​private String username; //账号private String password; //密码private List<SimpleGrantedAuthority> authorities; //拥有的权限@Overridepublic boolean isAccountNonExpired() { //账号是否未过期return true; }​@Overridepublic boolean isAccountNonLocked() { //账号是否未被锁定return true; }​@Overridepublic boolean isCredentialsNonExpired() {//凭据是否未过期return true; }​@Overridepublic boolean isEnabled() {//账号是否可用return true; }}

1.5 完善用户信息服务

@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = new User();user.setUsername("admin");user.setPassword(passwordEncoder.encode("123456"));user.setAuthorities(Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"), new SimpleGrantedAuthority("ROLE_USER")));return user;}

1.6 完善认证管理器配置

//认证管理器配置@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//数据库数据认证提供器DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();//设置认证使用的用户详情服务,业就是查询用户信息的服务daoAuthenticationProvider.setUserDetailsService(userService);//设置密码使用的加密器daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());//设置认证管理构建器使用的认证提供器auth.authenticationProvider(daoAuthenticationProvider);}

1.7 HTTP认证配置

package com.qf.authentication.config;​import com.qf.authentication.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;​@EnableWebSecurity //启用securitypublic class SecurityConfig extends WebSecurityConfigurerAdapter {​@Autowiredprivate UserService userService;​//创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder(); }​//认证管理器配置@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//数据库数据认证提供器DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();//设置认证使用的用户详情服务,业就是查询用户信息的服务daoAuthenticationProvider.setUserDetailsService(userService);//设置密码使用的加密器daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());//设置认证管理构建器使用的认证提供器auth.authenticationProvider(daoAuthenticationProvider); }​//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();//关闭跨站请求模拟//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler().failureHandler().permitAll();http.authorizeRequests().anyRequest().authenticated();//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制http.logout().invalidateHttpSession(true).permitAll(); }}​

1.8 创建认证处理器

package com.qf.authentication.handler;​import org.springframework.security.core.Authentication;import org.springframework.security.web.authentication.AuthenticationSuccessHandler;import org.springframework.stereotype.Component;​import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;​//认证成功的处理器@Componentpublic class LoginSuccessHandler implements AuthenticationSuccessHandler {​@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.sendRedirect("/main.html"); }}​​package com.qf.authentication.handler;​import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.authentication.AuthenticationFailureHandler;import org.springframework.stereotype.Component;​import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;​//认证失败的处理器@Componentpublic class LoginFailureHandler implements AuthenticationFailureHandler {​​@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {response.sendRedirect("/"); }}

1.9 完善HTTP认证配置

//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();//关闭跨站请求模拟//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();http.authorizeRequests().anyRequest().authenticated();//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制http.logout().invalidateHttpSession(true).permitAll();}

1.10 页面创建

index.html

<html lang="en"><head><meta charset="UTF-8"><title>Security登录</title></head><body><form action="login" method="post"><input type="text" name="username"><input type="password" name="password"><input type="submit" value="登录"></form></body></html>

main.html

<html lang="en"><head><meta charset="UTF-8"><title>Security登录成功</title></head><body> 认证通过了</body></html>

1.11 启动程序进行测试

1.12 核心流程梳理

登录请求被 UsernamePasswordAuthenticationFilter 拦截,该拦截器尝试认证,认证过程中调用 AuthenticationManager进行认证。AuthenticationManager进行认证时,将该认证管理器中的所有认证提供器遍历一遍,遍历过程中,首先检测认证提供器是否支持认证的票据类型,如果支持,则认证提供器开始进行认证。认证提供器认证过程中会调用 UserDetailsService 获取用户信息,然后进行信息比对,如果正确,则返回一个认证通过的票据。所有认证提供器中,只要任意一个认证提供器认证通过,则表示认证成功。

2. 短信认证

2.1 创建短信认证过滤器

package com.qf.authentication.sms;​import org.springframework.lang.Nullable;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;import org.springframework.security.web.util.matcher.AntPathRequestMatcher;​import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;​//短信认证提供器,模仿UsernamePasswordAuthenticationFilter编写public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {​//短信登录使用的URL,请求类型必须时POSTprivate static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms","POST");​​public SmsAuthenticationFilter() {super(DEFAULT_ANT_PATH_REQUEST_MATCHER); }​public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); }​//尝试认证@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException, IOException, ServletException {return null; }​​//获取短信验证码@Nullableprotected String obtainCode(HttpServletRequest request) {return request.getParameter("code"); }​//获取手机号码@Nullableprotected String obtainMobile(HttpServletRequest request) {return request.getParameter("mobile"); }}

2.2 创建短信认证票据

package com.qf.authentication.sms;​import org.springframework.security.authentication.AbstractAuthenticationToken;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.SpringSecurityCoreVersion;import org.springframework.util.Assert;​import java.util.Collection;​//短信认证的票据public class SmsAuthenticationToken extends AbstractAuthenticationToken {​private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;​//认证之前存储手机号码,认证之后存储的是用户信息,也就是一个User对象private final Object principal;​//验证码private Object credentials;​/** * 认证之前使用 * @param principal */public SmsAuthenticationToken(Object principal, Object credentials){super(null);this.principal = principal;this.credentials = credentials;super.setAuthenticated(false); }​/** * 认证之后使用 * @param principal * @param authorities */public SmsAuthenticationToken(Object principal, Object credentials, Collectionextends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true); }​@Overridepublic Object getCredentials() {return credentials; }​@Overridepublic Object getPrincipal() {return principal; }​//这个方法是security框架执行认证流程时调用的,用户不应该调用,应该使用构造方法完成认证@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {Assert.isTrue(!isAuthenticated,"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");super.setAuthenticated(false); }​@Overridepublic void eraseCredentials() {super.eraseCredentials();this.credentials = null; }}

2.3 创建短信实体

package com.qf.authentication.sms;​import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;​@Data@AllArgsConstructor@NoArgsConstructorpublic class SmsCode {//手机号private String mobile;//验证码private String code;//过期时间private long expire;}

2.4 短信模拟

package com.qf.authentication.controller;​import com.qf.authentication.sms.SmsCode;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;​import javax.imageio.ImageIO;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;import java.awt.*;import java.awt.image.BufferedImage;import java.io.IOException;import java.util.Random;​​@Controllerpublic class SmsController {​@GetMapping("/code")public void imgCode(@RequestParam("mobile")String mobile, HttpSession session, HttpServletResponse response) throws IOException {BufferedImage bi = new BufferedImage(200, 40, BufferedImage.TYPE_INT_RGB);Graphics graphics = bi.getGraphics();graphics.setColor(Color.GRAY);graphics.fillRect(0, 0, 200, 40);StringBuilder builder = new StringBuilder();Random r = new Random();for(int i=0; i<6; i++){int num = r.nextInt(10);builder.append(num);graphics.setColor(Color.red);graphics.drawString(Integer.toString(num), i*10 + 20, 15); }//创建短信实体SmsCode code = new SmsCode(mobile, builder.toString(), System.currentTimeMillis() + 5 * 60 * 1000);//将短信实体放入session中session.setAttribute("smsCode", code);graphics.dispose();ImageIO.write(bi, "jpg", response.getOutputStream()); }}

2.5 完善短信认证过滤器

//尝试认证@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if(!request.getMethod().equalsIgnoreCase("POST")){throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); }String code = obtainCode(request);//获取短信验证码//从session中获取发送的短信验证码信息SmsCode smsCode = (SmsCode) request.getSession().getAttribute("smsCode");if(code == null || smsCode == null){throw new AuthenticationServiceException("SMS code cannot be null"); }String mobile = obtainMobile(request);//获取手机号long currentTime = System.currentTimeMillis();//获取系统当前时间if(smsCode.getExpire() < currentTime || !mobile.equals(smsCode.getMobile())){//如果系统当前时间比验证码过期时间还要大,说明验证码过期,手机号码与验证码不匹配throw new AuthenticationServiceException("SMS code is invalid:" + smsCode.getCode()); } else if(!code.equals(smsCode.getCode())){throw new AuthenticationServiceException("SMS code error"); }SmsAuthenticationToken token = new SmsAuthenticationToken(mobile, code);//创建SMS tokenthis.setDetails(request, token);return this.getAuthenticationManager().authenticate(token);//调用认证管理器认证token}​//将请求信息放入token中protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));}

2.6 创建短信认证提供器

package com.qf.authentication.sms;​import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;​public class SmsAuthenticationProvider implements AuthenticationProvider {​//获取认证用户的信息的服务接口private UserDetailsService userDetailsService;​/** * 这个方法就是认证,如果没有抛出认证异常,说明认证成功 * @param authentication未进行认证的信息,里面就是包含了一个mobile信息和请求的信息 * @return 返回一个认证完成的信息 * @throws AuthenticationException */@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {String mobile = (String) authentication.getPrincipal();UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); //通过手机号码获取用户信息SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities());authenticationToken.setDetails(authentication.getDetails());return authenticationToken; }​@Overridepublic boolean supports(Class authentication) {// 判断 authentication 是不是 SmsCodeAuthenticationToken 类型或者其子类或者其子接口return SmsAuthenticationToken.class.isAssignableFrom(authentication); }​public UserDetailsService getUserDetailsService() {return userDetailsService; }​public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService; }}

2.7 配置短信认证

package com.qf.authentication.config;​import com.qf.authentication.handler.LoginFailureHandler;import com.qf.authentication.handler.LoginSuccessHandler;import com.qf.authentication.service.UserService;import com.qf.authentication.sms.SmsAuthenticationFilter;import com.qf.authentication.sms.SmsAuthenticationProvider;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.http.HttpMethod;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;​@EnableWebSecurity //启用securitypublic class SecurityConfig extends WebSecurityConfigurerAdapter {​@Autowiredprivate UserService userService;​@Autowiredprivate LoginSuccessHandler loginSuccessHandler;​@Autowiredprivate LoginFailureHandler loginFailureHandler;​@Autowired@Qualifier("authenticationManagerBean") //表示使用指定名称的认证管理器private AuthenticationManager authenticationManager;​//创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder(); }​//认证管理器配置@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//创建短信认证提供器SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();smsAuthenticationProvider.setUserDetailsService(userService);//将认证提供器添加到认证管理器中auth.authenticationProvider(smsAuthenticationProvider);​//数据库数据认证提供器DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();//设置认证使用的用户详情服务,业就是查询用户信息的服务daoAuthenticationProvider.setUserDetailsService(userService);//设置密码使用的加密器daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());//设置认证管理构建器使用的认证提供器auth.authenticationProvider(daoAuthenticationProvider); }​//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();//关闭跨站请求模拟//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();//设置获取验证码的请求放行http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll()//其他请求需要认证 .anyRequest().authenticated();//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制http.logout().invalidateHttpSession(true).permitAll();//将短信认证过滤器添加账号密码过滤器的前面http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); }​@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean(); }​@Beanpublic SmsAuthenticationFilter smsAuthenticationFilter() throws Exception {SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();//设置短信认证过滤器使用的认证管理器smsAuthenticationFilter.setAuthenticationManager(authenticationManager);//设置登录成功的处理器smsAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);//设置登录失败的处理器smsAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler);return smsAuthenticationFilter; }}

2.8 修改登录页面

index.html

<html lang="en"><head><meta charset="UTF-8"><title>Security登录</title></head><body><form action="login" method="post"><input type="text" name="username"><input type="password" name="password"><input type="submit" value="登录"></form><form action="sms" method="post" ><input type="text" name="mobile"><input type="text" name="code"><input type="button" value="获取验证码" onclick="getCode()"><input type="submit" value="登录"></form></body><script type="text/javascript">function getCode(){let elements = document.getElementsByName("code");let img = document.createElement("img");img.src = "code?mobile=" + document.getElementsByName("mobile")[0].value;elements[0].after(img); }</script></html>

2.9 启动程序进行测试

第四节 Spring Security 授权

1. 启用注解授权

@EnableWebSecurity //启用security//prePostEnabled = true启用@PreAuthorize()//securedEnabled = true启用@Secured()//jsr250Enabled = true启用@RolesAllowed、@PermitAll、@DenyAll 但该注解需要jar包支撑@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {}

JSR 250 依赖包

<dependency><groupId>javax.annotation</groupId><artifactId>jsr250-api</artifactId><version>1.0</version></dependency>

2. HTTP授权配置

//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();//关闭跨站请求模拟//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();//设置获取验证码的请求放行http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll() //授权请求表示任意请求都需要认证才能够访问 .anyRequest().authenticated();//设置异常处理使用访问拒绝处理器http.exceptionHandling().accessDeniedHandler();//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制http.logout().invalidateHttpSession(true).permitAll();//将短信认证过滤器添加账号密码过滤器的前面http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}

3. 创建拒绝请求处理器

package com.qf.authentication.handler;​import org.springframework.security.access.AccessDeniedException;import org.springframework.security.web.access.AccessDeniedHandler;import org.springframework.stereotype.Component;​import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;​//HTTP请求被拒绝的处理器@Componentpublic class RequestDeniedHandler implements AccessDeniedHandler {​//这里就是拒绝处理的具体步骤实现@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("text/html;charset=utf-8");//返回拒绝处理的信息response.getWriter().print(accessDeniedException.getMessage()); }}

5. 完善HTTP授权配置

@Autowiredprivate RequestDecisionManager decisionManager;​@Autowiredprivate RequestDeniedHandler deniedHandler;​//Http认证配置@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable();//关闭跨站请求模拟//设置表单登录使用的登录地址、登录请求的URL地址、登录成功和失败分别使用的处理器 permitAll表示该操作不需要security的权限控制http.formLogin().loginPage("/").loginProcessingUrl("/login") .successHandler(loginSuccessHandler).failureHandler(loginFailureHandler).permitAll();//设置获取验证码的请求放行http.authorizeRequests().antMatchers(HttpMethod.GET, "/code").permitAll() //授权请求表示任意请求都需要认证才能够访问 .anyRequest().authenticated();//设置异常处理使用访问拒绝处理器http.exceptionHandling().accessDeniedHandler(deniedHandler);//设置退出操作使当前session失效 permitAll表示该操作不需要security的权限控制http.logout().invalidateHttpSession(true).permitAll();//将短信认证过滤器添加账号密码过滤器的前面http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}

6. 创建测试请求

package com.qf.authentication.controller;​import org.springframework.security.access.annotation.Secured;import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;​import javax.annotation.security.RolesAllowed;​@RestControllerpublic class TestController {​@GetMapping("/test1")@PreAuthorize("hasRole('ROLE_ADMIN')")public String test1(){return "test1"; }​@GetMapping("/test2")@PreAuthorize("hasRole('ROLE_TEST')")public String test2(){return "test2"; }​@GetMapping("/test3")@Secured("ROLE_ADMIN")public String test3(){return "test3"; }​@GetMapping("/test4")@Secured("ROLE_TEST")public String test4(){return "test4"; }​@GetMapping("/test5")@RolesAllowed("ROLE_ADMIN")public String test5(){return "test5"; }​@GetMapping("/test6")@RolesAllowed("ROLE_TEST")public String test6(){return "test6"; }}

7. 启动程序进行测试

第五节 Security 与 AJAX 对接

这个问题的根本原因在于登录结果的处理和拒绝访问的处理。如果能够判断一个请求是ajax请求,那么问题即将得到解决。

package com.qf.security.util;​import javax.servlet.http.HttpServletRequest;​/** * @Author: wu * @Description: * @Date: 2021-11-09 *///针对请求相关的操作工具类public class RequestUtil {​private RequestUtil(){}​/** * 验证请求是否是AJAX请求 这种验证对于jQuery发送的AJAX没有任何问题 * 但是,对于 axios发送的AJAX可能存在没有X-Requested-With这个头信 * 息的 * @param request * @return */public static boolean isAjaxRequest(HttpServletRequest request){String header = request.getHeader("X-Requested-With");return "XMLHttpRequest".equalsIgnoreCase(header); }}

登录的时候传递的参数在过滤器中获取不到,需要注意:在传递参数的时候要使用get方法传递参数的方式对参数进行拼接,然后赋值给data

$.ajax({type: 'post',url: 'login',data: "username="+ $("#username").val() + "&password=" + $("#password").val(),success: function (resp) {console.log(resp); }});