Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。
用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
//这个接口的实现类都是Token,称为票据,UsernamePasswordAuthenticationToken //我们自定义实现就可以叫MobileCodeAuthenticationToken public interface Authentication extends Principal, Serializable { /** * 权限列表 */ Collection extends GrantedAuthority> getAuthorities(); /** * 认证凭据,可能是密码,也可能是验证码,也可能是其他认证凭据 */ Object getCredentials(); /** * 认证请求的详细信息,可能是IP地址,也可能是认证序列号,也可能是null */ Object getDetails(); /** * 如果通过认证,则返回的是包含(用户名和密码)或者(手机号和验证码)等的对象;如果认证不通过, * 则返回的是用户名或者手机号等。 */ Object getPrincipal(); /** * 是否认证通过 */ boolean isAuthenticated(); /** * 修改认证状态 */ void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
Authentication接口就是用来携带认证信息的。认证信息包括用户身份信息,密码,及权限列表等
账号密码认证过滤器,用于认证用户信息,认证方式是由 AuthenticationManager 接口提供。
public interface AuthenticationManager { /** * 尝试认证传递过来的认证信息,如果认证成功,则会修改认证信息的状态。否则,则会抛出异常 */ Authentication authenticate(Authentication authentication) throws AuthenticationException; }
认证管理器,是认证相关的核心接口,也是发起认证的出发点。实际业务中可能根据不同的信息进行认证,所以Spring推荐通过实现 AuthenticationManager 接口来自定义自己的认证方式。Spring 提供了一个默认的实现 ProviderManager。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { //省略其他属性 private Listproviders = Collections.emptyList(); //省略其他内容 }
认证提供者管理器,该类中维护了一个认证提供者列表,只要这个列表中的任何一个认证提供者提供的认证方式认证通过,认证就结束。
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; //用户详细信息服务,主要用于查询认证用户信息 //省略其他内容 }
public interface UserDetailsService { /** * 根据用户名获取用户详细信息 */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
public interface UserDetails extends Serializable { /** * 用户拥有的权限列表 */ Collection extends GrantedAuthority> getAuthorities(); /** * 密码 */ String getPassword(); /** * 账号 */ String getUsername(); /** * 账号是否过期,过期的账号不能进行认证 */ boolean isAccountNonExpired(); /** * 账号是否被锁定 */ boolean isAccountNonLocked(); /** * 凭据是否过期,过期的凭据不能进行认证 */ boolean isCredentialsNonExpired(); /** * 账号是否被启用,未启用的账号不能进行认证 */ boolean isEnabled(); }
用户的详细信息,主要用于登录认证。
SecurityContextHolder 是最基本的对象,它负责存储当前 SecurityContext 信息。SecurityContextHolder默认使用 ThreadLocal 来存储认证信息,意味着这是一种与线程绑定的策略。在Web场景下的使用Spring Security,在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。
SecurityContext 负责存储认证通过的用户信息(Authentication对象),保存着当前用户是什么,是否已经通过认证,拥有哪些权限等等。
AuthenticationSuccessHandler 主要用于认证成功后的处理,比如返回页面或者数据。
AuthenticationFailureHandler 主要用于认证失败后的处理,比如返回页面或者数据。
AccessDecisionManager 主要用于实现权限,决定请求是否具有访问的权限。
AccessDeniedHandler 主要用于无权访问时的处理
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 容器管理。如果DelegatingFilterProxy在web.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 的值。
springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy targetBeanName springSecurityFilterChain springSecurityFilterChain /*
使用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 ,用户将不必填写用户名和密码,而是直接登录进入系统,该过滤器默认从不开启
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 //启用security public class SecurityConfig extends WebSecurityConfigurerAdapter { //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //认证管理器配置 @Override protected 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; @Service public class UserServiceImpl implements UserService { @Override public 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; @Data public class User implements UserDetails { private String username; //账号 private String password; //密码 private Listauthorities; //拥有的权限 @Override public boolean isAccountNonExpired() { //账号是否未过期 return true; } @Override public boolean isAccountNonLocked() { //账号是否未被锁定 return true; } @Override public boolean isCredentialsNonExpired() {//凭据是否未过期 return true; } @Override public boolean isEnabled() {//账号是否可用 return true; } }
1.5 完善用户信息服务
@Override public 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 完善认证管理器配置
//认证管理器配置 @Override protected 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 //启用security public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //认证管理器配置 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //数据库数据认证提供器 DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); //设置认证使用的用户详情服务,业就是查询用户信息的服务 daoAuthenticationProvider.setUserDetailsService(userService); //设置密码使用的加密器 daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); //设置认证管理构建器使用的认证提供器 auth.authenticationProvider(daoAuthenticationProvider); } //Http认证配置 @Override protected 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; //认证成功的处理器 @Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public 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; //认证失败的处理器 @Component public class LoginFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.sendRedirect("/"); } }
1.9 完善HTTP认证配置
//Http认证配置 @Override protected 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
Security登录
main.html
Security登录成功 认证通过了
1.11 启动程序进行测试
1.12 核心流程梳理
登录请求被 UsernamePasswordAuthenticationFilter 拦截,该拦截器尝试认证,认证过程中调用 AuthenticationManager进行认证。AuthenticationManager进行认证时,将该认证管理器中的所有认证提供器遍历一遍,遍历过程中,首先检测认证提供器是否支持认证的票据类型,如果支持,则认证提供器开始进行认证。认证提供器认证过程中会调用 UserDetailsService 获取用户信息,然后进行信息比对,如果正确,则返回一个认证通过的票据。所有认证提供器中,只要任意一个认证提供器认证通过,则表示认证成功。
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,请求类型必须时POST private 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); } //尝试认证 @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { return null; } //获取短信验证码 @Nullable protected String obtainCode(HttpServletRequest request) { return request.getParameter("code"); } //获取手机号码 @Nullable protected 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, Collection extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return credentials; } @Override public Object getPrincipal() { return principal; } //这个方法是security框架执行认证流程时调用的,用户不应该调用,应该使用构造方法完成认证 @Override public 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); } @Override public 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 @NoArgsConstructor public 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; @Controller public 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 完善短信认证过滤器
//尝试认证 @Override public 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 token this.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 */ @Override public 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; } @Override public 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 //启用security public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private LoginSuccessHandler loginSuccessHandler; @Autowired private LoginFailureHandler loginFailureHandler; @Autowired @Qualifier("authenticationManagerBean") //表示使用指定名称的认证管理器 private AuthenticationManager authenticationManager; //创建密码加密器,并纳入Spring IOC容器管理,该Bean的名字就是方法名 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } //认证管理器配置 @Override protected 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认证配置 @Override protected 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 @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception { SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); //设置短信认证过滤器使用的认证管理器 smsAuthenticationFilter.setAuthenticationManager(authenticationManager); //设置登录成功的处理器 smsAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler); //设置登录失败的处理器 smsAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler); return smsAuthenticationFilter; } }
2.8 修改登录页面
index.html
Security登录
2.9 启动程序进行测试
@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 依赖包
javax.annotation jsr250-api 1.0
//Http认证配置 @Override protected 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); }
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请求被拒绝的处理器 @Component public class RequestDeniedHandler implements AccessDeniedHandler { //这里就是拒绝处理的具体步骤实现 @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("text/html;charset=utf-8"); //返回拒绝处理的信息 response.getWriter().print(accessDeniedException.getMessage()); } }
@Autowired private RequestDecisionManager decisionManager; @Autowired private RequestDeniedHandler deniedHandler; //Http认证配置 @Override protected 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); }
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; @RestController public 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"; } }
这个问题的根本原因在于登录结果的处理和拒绝访问的处理。如果能够判断一个请求是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); } });