Spring Security
是一种基于Spring AOP
和Servlet Filter
的安全框架,其核心是一组过滤器链,实现 Web 请求和方法调用级别的用户鉴权和权限控制。本文将会介绍该安全框架的身份认证和退出登录的基本用法,并对其相关源码进行分析。
表单认证
Spring Security
提供了两种认证方式:HttpBasic 认证和 HttpForm 表单认证。HttpBasic 认证不需要我们编写登录页面,当浏览器请求 URL 需要认证才能访问时,页面会自动弹出一个登录窗口,要求用户输入用户名和密码进行认证。大多数情况下,我们还是通过编写登录页面进行 HttpForm 表单认证。
快速入门
☕️ 工程的整体目录
☕️ 在 pom.xml 添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.3.3.RELEASE</version > <relativePath /> </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > <version > 3.10</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > <exclusions > <exclusion > <groupId > org.junit.vintage</groupId > <artifactId > junit-vintage-engine</artifactId > </exclusion > </exclusions > </dependency > </dependencies >
☕️ 在 application.properties 添加配置
1 2 spring.thymeleaf.cache =false
☕️ 编写 Controller 层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.example.contorller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller public class HomeController { @GetMapping({"/", "/index"}) @ResponseBody public String index () { return "欢迎您登录!!!" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.example.contorller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;@Controller public class LoginController { @GetMapping("/login/page") public String loginPage () { return "login" ; } }
☕️ 编写 login.html 页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!DOCTYPE html > <html lang ="en" xmlns:th ="http://www.thymeleaf.org" > <head > <meta charset ="UTF-8" > <title > 登录</title > </head > <body > <h3 > 表单登录</h3 > <form method ="post" th:action ="@{/login/form}" > <input type ="text" name ="name" placeholder ="用户名" > <br > <input type ="password" name ="pwd" placeholder ="密码" > <br > <div th:if ="${param.error}" > <span th:text ="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style ="color:red" > 用户名或密码错误</span > </div > <button type ="submit" > 登录</button > </form > </body > </html >
☕️ 编写安全配置类 SpringSecurityConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package com.example.config;import org.springframework.context.annotation.Bean;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;@EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public BCryptPasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("admin" ).password(passwordEncoder() .encode("123456" )).roles("ROLE_ADMIN" ); } @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login/page" ) .loginProcessingUrl("/login/form" ) .usernameParameter("name" ) .passwordParameter("pwd" ) .defaultSuccessUrl("/index" ) .failureUrl("/login/page?error" ); http.authorizeRequests() .antMatchers("/login/page" ).permitAll() .anyRequest().authenticated(); http.csrf().disable(); } @Override public void configure (WebSecurity web) throws Exception { web.ignoring().antMatchers("/**/*.css" , "/**/*.js" , "/**/*.png" , "/**/*.jpg" , "/**/*.jpeg" ); } }
上述的安全配置类继承了WebSecurityConfigurerAdapter
抽象类,并重写了三个重载的 configure() 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void configure (AuthenticationManagerBuilder auth) ;void configure (HttpSecurity http) ;void configure (WebSecurity web) ;
安全配置类需要使用 @EnableWebSecurity 注解修饰,该注解是一个组合注解,内部包含了 @Configuration 注解,所以安全配置类不需要添加 @Configuration 注解即可被 Spring 容器识别。具体定义如下:
1 2 3 4 5 6 7 8 9 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class}) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { boolean debug () default false ; }
☕️ 测试
启动项目,访问localhost:8080
,重定向到/login/page
登录页面要求身份认证:
输入正确的用户名和密码认证成功后,重定向到原始访问路径:
UserDetailsService 和 UserDetails 接口
⭐️ UserDetailService 接口
该接口只有一个方法 loadUserByUsername(),用于定义从数据库中获取指定用户信息的逻辑。如果未获取到用户信息,则需要手动抛出 UsernameNotFoundException 异常;如果获取到用户信息,则将该用户信息封装到 UserDetails 接口的实现类中并返回。
1 2 3 4 public interface UserDetailsService { UserDetails loadUserByUsername (String username) throws UsernameNotFoundException ; }
⭐️ UserDetails 接口
UserDetails 接口定义了用于描述用户信息的方法,具体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword () ; String getUsername () ; boolean isAccountNonExpired () ; boolean isAccountNonLocked () ; boolean isCredentialsNonExpired () ; boolean isEnabled () ; }
自定义用户认证
✏️ 数据库准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 DROP TABLE IF EXISTS `user `;CREATE TABLE `user ` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键' , `username` varchar (50 ) NOT NULL COMMENT '用户名' , `password` varchar (64 ) COMMENT '密码' , `mobile` varchar (20 ) COMMENT '手机号' , `enabled` tinyint NOT NULL DEFAULT '1' COMMENT '用户是否可用' , `roles` text COMMENT '用户角色,多个角色之间用逗号隔开' , PRIMARY KEY (`id`), KEY `index_username`(`username`), KEY `index_mobile`(`mobile`) ) COMMENT '用户表' ; INSERT INTO `user ` VALUES ('1' , 'admin' , '$2a$10$JNVWTh5Yq56kJtrCZkcDk.DL/L/i8g3KrTAshcHW3mFf8//lnfG56' , '11111111111' , '1' , 'ROLE_ADMIN,ROLE_USER' );INSERT INTO `user ` VALUES ('2' , 'user' , '$2a$10$JNVWTh5Yq56kJtrCZkcDk.DL/L/i8g3KrTAshcHW3mFf8//lnfG56' , '22222222222' , '1' , 'ROLE_USER' );
我们将用户信息和角色信息放在同一张表中,roles 字段设定为 text 类型,多个角色之间用逗号隔开。
✏️ 在 pom.xml 中添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.1.3</version > </dependency >
✏️ 在 application.properties 中添加配置
1 2 3 4 5 6 7 8 9 10 11 12 spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/spring_security_test?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong spring.datasource.username =root spring.datasource.password =123456 mybatis.configuration.map-underscore-to-camel-case =true mybatis.mapper-locations =classpath*:/mapper/**/*.xml mybatis.type-aliases-package =com.example.entity
✏️ 创建 User 实体类,实现 UserDetails 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.example.entity;import lombok.Data;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;import java.util.List;@Data public class User implements UserDetails { private Long id; private String username; private String password; private String mobile; private String roles; private boolean enabled; private List<GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return enabled; } @Override public boolean equals (Object obj) { return obj instanceof User && this .username.equals(((User) obj).username); } @Override public int hashCode () { return this .username.hashCode(); } }
✏️ 创建 UserMapper 接口
1 2 3 4 5 6 7 8 9 package com.example.mapper;import com.example.entity.User;import org.apache.ibatis.annotations.Select;public interface UserMapper { @Select("select * from user where username = #{username}") User selectByUsername (String username) ; }
Mapper 接口需要注册到 Spring 容器中,所以在启动类上添加 Mapper 的包扫描路径:
1 2 3 4 5 6 7 8 @SpringBootApplication @MapperScan("com.example.mapper") public class Application { public static void main (String[] args) { SpringApplication.run(Application.class, args); } }
✏️ 创建 CustomUserDetailsService 类,实现 UserDetailsService 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.example.service;import com.example.entity.User;import com.example.mapper.UserMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { User user = userMapper.selectByUsername(username); if (user == null ) { throw new UsernameNotFoundException("用户不存在" ); } user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles())); return user; } }
✏️ 修改安全配置类 SpringSecurityConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login/page" ).permitAll() .antMatchers("/admin/**" ).hasRole("ADMIN" ) .antMatchers("/user/**" ).hasAuthority("ROLE_USER" ) .anyRequest().authenticated(); } }
此处需要简单介绍下Spring Security
的授权方式,在Spring Security
中角色属于权限的一部分。对于角色ROLE_ADMIN
的授权方式有两种:hasRole("ADMIN")
和hasAuthority("ROLE_ADMIN")
,这两种方式是等价的。可能有人会疑惑,为什么在数据库中的角色名添加了ROLE_
前缀,而 hasRole() 配置时不需要加ROLE_
前缀,我们查看相关源码:
1 2 3 4 5 6 7 8 private static String hasRole (String role) { Assert.notNull(role, "role cannot be null" ); if (role.startsWith("ROLE_" )) { throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'" ); } else { return "hasRole('ROLE_" + role + "')" ; } }
✏️ 创建 AdminController 和 UserController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.example.contorller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller @RequestMapping("/admin") public class AdminController { @GetMapping("/hello") @ResponseBody public String hello () { return "hello,admin!!!" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.example.contorller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller @RequestMapping("/user") public class UserController { @GetMapping("/hello") @ResponseBody public String hello () { return "hello,User!!!" ; } }
✏️ 测试
访问localhost:8080/user/hello
,重定向到/login/page
登录页面要求身份认证:
用户名输入 user,密码输入 123456,认证成功后重定向到原始访问路径:
访问localhost:8080/admin/hello
,访问受限,页面显示 403。
基本流程分析
Spring Security
采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用Spring Security
提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在configure(HttpSecurity http)
方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:
UsernamePasswordAuthenticationFilter
过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。
ExceptionTranslationFilter
过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
FilterSecurityInterceptor
过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter
过滤器进行捕获和处理。
认证流程
认证流程是在UsernamePasswordAuthenticationFilter
过滤器中处理的,具体流程如下所示:
☕️ UsernamePasswordAuthenticationFilter
源码
当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认证。该过滤器的 doFilter() 方法实现在其抽象父类AbstractAuthenticationProcessingFilter
中,查看相关源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware , MessageSourceAware { public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (!this .requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { if (this .logger.isDebugEnabled()) { this .logger.debug("Request is to process authentication" ); } Authentication authResult; try { authResult = this .attemptAuthentication(request, response); if (authResult == null ) { return ; } this .sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException var8) { this .logger.error("An internal error occurred while trying to authenticate the user." , var8); this .unsuccessfulAuthentication(request, response, var8); return ; } catch (AuthenticationException var9) { this .unsuccessfulAuthentication(request, response, var9); return ; } if (this .continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } this .successfulAuthentication(request, response, chain, authResult); } } }
上述的(2)过程调用了UsernamePasswordAuthenticationFilter
的 attemptAuthentication() 方法,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username" ; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password" ; private String usernameParameter = "username" ; private String passwordParameter = "password" ; private boolean postOnly = true ; public UsernamePasswordAuthenticationFilter () { super (new AntPathRequestMatcher("/login" , "POST" )); } public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this .postOnly && !request.getMethod().equals("POST" )) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this .obtainUsername(request); String password = this .obtainPassword(request); if (username == null ) { username = "" ; } if (password == null ) { password = "" ; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this .setDetails(request, authRequest); return this .getAuthenticationManager().authenticate(authRequest); } } }
上述的(3)过程创建的UsernamePasswordAuthenticationToken
是 Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认证的用户信息,一个用于封装认证成功后的用户信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 530L ; private final Object principal; private Object credentials; public UsernamePasswordAuthenticationToken (Object principal, Object credentials) { super ((Collection)null ); this .principal = principal; this .credentials = credentials; this .setAuthenticated(false ); } public UsernamePasswordAuthenticationToken (Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super (authorities); this .principal = principal; this .credentials = credentials; super .setAuthenticated(true ); } }
Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface Authentication extends Principal , Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials () ; Object getDetails () ; Object getPrincipal () ; boolean isAuthenticated () ; void setAuthenticated (boolean var1) throws IllegalArgumentException ; }
☕️ ProviderManager 源码
上述过程中,UsernamePasswordAuthenticationFilter
过滤器的 attemptAuthentication() 方法的(5)过程将未认证的 Authentication 对象传入 ProviderManager 类的 authenticate() 方法进行身份认证。
ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是 AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个List<AuthenticationProvider>
列表,存放多种认证方式,实际上这是委托者模式(Delegate)的应用。每种认证方式对应着一个 AuthenticationProvider,AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托对应的 AuthenticationProvider 进行用户认证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 public class ProviderManager implements AuthenticationManager , MessageSourceAware , InitializingBean { public Authentication authenticate (Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null ; AuthenticationException parentException = null ; Authentication result = null ; Authentication parentResult = null ; boolean debug = logger.isDebugEnabled(); Iterator var8 = this .getProviders().iterator(); while (var8.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var8.next(); if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null ) { this .copyDetails(authentication, result); break ; } } catch (InternalAuthenticationServiceException | AccountStatusException var13) { this .prepareException(var13, authentication); throw var13; } catch (AuthenticationException var14) { lastException = var14; } } } if (result == null && this .parent != null ) { try { result = parentResult = this .parent.authenticate(authentication); } catch (ProviderNotFoundException var11) { } catch (AuthenticationException var12) { parentException = var12; lastException = var12; } } if (result != null ) { if (this .eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null ) { this .eventPublisher.publishAuthenticationSuccess(result); } return result; } else { if (lastException == null ) { lastException = new ProviderNotFoundException(this .messages.getMessage("ProviderManager.providerNotFound" , new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}" )); } if (parentException == null ) { this .prepareException((AuthenticationException)lastException, authentication); } throw lastException; } } }
上述认证成功之后的(6)过程,调用 CredentialsContainer 接口定义的 eraseCredentials() 方法去除敏感信息。查看 UsernamePasswordAuthenticationToken 实现的 eraseCredentials() 方法,该方法实现在其父类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public abstract class AbstractAuthenticationToken implements Authentication , CredentialsContainer { public void eraseCredentials () { this .eraseSecret(this .getCredentials()); this .eraseSecret(this .getPrincipal()); this .eraseSecret(this .details); } private void eraseSecret (Object secret) { if (secret instanceof CredentialsContainer) { ((CredentialsContainer)secret).eraseCredentials(); } } }
☕️ DaoAuthenticationProvider 源码
上述的(4)过程,ProviderManager 将未认证的 Authentication 对象委托给 DaoAuthenticationProvider 进行身份认证。该类的 authenticate() 方法实现在其抽象父类 AbstractUserDetailsAuthenticationProvider 中,其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider , InitializingBean , MessageSourceAware { public Authentication authenticate (Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this .messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports" , "Only UsernamePasswordAuthenticationToken is supported" ); }); String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true ; UserDetails user = this .userCache.getUserFromCache(username); if (user == null ) { cacheWasUsed = false ; try { user = this .retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { this .logger.debug("User '" + username + "' not found" ); if (this .hideUserNotFoundExceptions) { throw new BadCredentialsException(this .messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } throw var6; } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract" ); } try { this .preAuthenticationChecks.check(user); this .additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; } cacheWasUsed = false ; user = this .retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this .preAuthenticationChecks.check(user); this .additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } this .postAuthenticationChecks.check(user); if (!cacheWasUsed) { this .userCache.putUserInCache(user); } Object principalToReturn = user; if (this .forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this .createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication (Object principal, Authentication authentication, UserDetails user) { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this .authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } }
上述的过程,调用了子类 DaoAuthenticationProvider 重写的方法,其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { protected final UserDetails retrieveUser (String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this .prepareTimingAttackProtection(); try { UserDetails loadedUser = this .getUserDetailsService().loadUserByUsername(username); if (loadedUser == null ) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation" ); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this .mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } } protected void additionalAuthenticationChecks (UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null ) { this .logger.debug("Authentication failed: no credentials provided" ); throw new BadCredentialsException(this .messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } else { String presentedPassword = authentication.getCredentials().toString(); if (!this .passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this .logger.debug("Authentication failed: password does not match stored value" ); throw new BadCredentialsException(this .messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials" , "Bad credentials" )); } } } protected Authentication createSuccessAuthentication (Object principal, Authentication authentication, UserDetails user) { boolean upgradeEncoding = this .userDetailsPasswordService != null && this .passwordEncoder.upgradeEncoding(user.getPassword()); if (upgradeEncoding) { String presentedPassword = authentication.getCredentials().toString(); String newPassword = this .passwordEncoder.encode(presentedPassword); user = this .userDetailsPasswordService.updatePassword(user, newPassword); } return super .createSuccessAuthentication(principal, authentication, user); } }
☕️ 认证成功/失败处理
上述过程就是认证流程的最核心部分,接下来重新回到UsernamePasswordAuthenticationFilter
过滤器的 doFilter() 方法,查看认证成功/失败的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware , MessageSourceAware { public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { try { authResult = this .attemptAuthentication(request, response); } catch (AuthenticationException var9) { this .unsuccessfulAuthentication(request, response, var9); return ; } this .successfulAuthentication(request, response, chain, authResult); } }
查看 successfulAuthentication() 和 unsuccessfulAuthentication() 方法源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware , MessageSourceAware { protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this .logger.isDebugEnabled()) { this .logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); this .rememberMeServices.loginSuccess(request, response, authResult); if (this .eventPublisher != null ) { this .eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this .getClass())); } this .successHandler.onAuthenticationSuccess(request, response, authResult); } protected void unsuccessfulAuthentication (HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (this .logger.isDebugEnabled()) { this .logger.debug("Authentication request failed: " + failed.toString(), failed); this .logger.debug("Updated SecurityContextHolder to contain null Authentication" ); this .logger.debug("Delegating to authentication failure handler " + this .failureHandler); } this .rememberMeServices.loginFail(request, response); this .failureHandler.onAuthenticationFailure(request, response, failed); } }
☕️ 认证流程中各核心类和接口的关系图
权限访问流程
上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对ExceptionTranslationFilter
过滤器和FilterSecurityInterceptor
过滤器进行介绍。
✌ExceptionTranslationFilter
过滤器
该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ExceptionTranslationFilter extends GenericFilterBean { public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; try { chain.doFilter(request, response); this .logger.debug("Chain processed normally" ); } catch (IOException var9) { throw var9; } catch (Exception var10) { Throwable[] causeChain = this .throwableAnalyzer.determineCauseChain(var10); RuntimeException ase = (AuthenticationException)this .throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null ) { ase = (AccessDeniedException)this .throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); } } } }
✌FilterSecurityInterceptor
过滤器
FilterSecurityInterceptor
是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter
进行捕获和处理。具体源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); this .invoke(fi); } public void invoke (FilterInvocation fi) throws IOException, ServletException { if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied" ) != null && this .observeOncePerRequest) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { if (fi.getRequest() != null && this .observeOncePerRequest) { fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied" , Boolean.TRUE); } InterceptorStatusToken token = super .beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super .finallyInvocation(token); } super .afterInvocation(token, (Object)null ); } } }
需要注意,Spring Security
的过滤器链是配置在 SpringMVC 的核心组件 DispatcherServlet 运行之前。也就是说,请求通过Spring Security
的所有过滤器,不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。
请求间共享认证信息
一般认证成功后的用户信息是通过 Session 在多个请求之间共享,那么Spring Security
中是如何实现将已认证的用户信息对象 Authentication 与 Session 绑定的,该部分会进行具体分析。
原理分析
✍ SecurityContext 和 SecurityContextHolder
在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码:
1 2 3 4 5 6 7 8 9 10 public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware , MessageSourceAware { protected void successfulAuthentication (HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult); } }
查看 SecurityContext 接口及其实现类 SecurityContextImpl,该类其实就是对 Authentication 的封装:
1 2 3 4 5 public interface SecurityContext extends Serializable { Authentication getAuthentication () ; void setAuthentication (Authentication var1) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class SecurityContextImpl implements SecurityContext { private static final long serialVersionUID = 520L ; private Authentication authentication; public SecurityContextImpl () { } public SecurityContextImpl (Authentication authentication) { this .authentication = authentication; } public Authentication getAuthentication () { return this .authentication; } public void setAuthentication (Authentication authentication) { this .authentication = authentication; } }
查看 SecurityContextHolder 类,该类其实是对 ThreadLocal 的封装,存储 SecurityContext 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public class SecurityContextHolder { private static SecurityContextHolderStrategy strategy; private static int initializeCount = 0 ; public SecurityContextHolder () { } static { initialize(); } private static void initialize () { if (!StringUtils.hasText(strategyName)) { strategyName = "MODE_THREADLOCAL" ; } if (strategyName.equals("MODE_THREADLOCAL" )) { strategy = new ThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL" )) { strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); } else if (strategyName.equals("MODE_GLOBAL" )) { strategy = new GlobalSecurityContextHolderStrategy(); } else { try { Class<?> clazz = Class.forName(strategyName); Constructor<?> customStrategy = clazz.getConstructor(); strategy = (SecurityContextHolderStrategy)customStrategy.newInstance(); } catch (Exception var2) { ReflectionUtils.handleReflectionException(var2); } } ++initializeCount; } public static SecurityContext getContext () { return strategy.getContext(); } public static void setContext (SecurityContext context) { strategy.setContext(context); } public static void clearContext () { strategy.clearContext(); } public static SecurityContextHolderStrategy getContextHolderStrategy () { return strategy; } public static SecurityContext createEmptyContext () { return strategy.createEmptyContext(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal(); ThreadLocalSecurityContextHolderStrategy() { } public SecurityContext getContext () { SecurityContext ctx = (SecurityContext)contextHolder.get(); if (ctx == null ) { ctx = this .createEmptyContext(); contextHolder.set(ctx); } return ctx; } public void setContext (SecurityContext context) { Assert.notNull(context, "Only non-null SecurityContext instances are permitted" ); contextHolder.set(context); } public void clearContext () { contextHolder.remove(); } public SecurityContext createEmptyContext () { return new SecurityContextImpl(); } }
☕️ SecurityContextPersistenceFilter
过滤器
前面提到过,在UsernamePasswordAuthenticationFilter
过滤器认证成功之后,会在认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进 SecurityContext,并存入 SecurityContextHolder。之后,响应会通过SecurityContextPersistenceFilter
过滤器,该过滤器的位置在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中处理已认证的用户信息对象 Authentication 与 Session 绑定。
认证成功的响应通过SecurityContextPersistenceFilter
过滤器时,会从 SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的 SecurityContext,放进 Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤器会判断当前请求的 Session 是否存有 SecurityContext 对象,如果有则将该对象取出再次放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访问不需要进行身份认证;当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出 SecurityContext 对象,放入 Session 中。具体源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class SecurityContextPersistenceFilter extends GenericFilterBean { public void doFilter (ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (request.getAttribute("__spring_security_scpf_applied" ) != null ) { chain.doFilter(request, response); } else { boolean debug = this .logger.isDebugEnabled(); request.setAttribute("__spring_security_scpf_applied" , Boolean.TRUE); if (this .forceEagerSessionCreation) { HttpSession session = request.getSession(); if (debug && session.isNew()) { this .logger.debug("Eagerly created session: " + session.getId()); } } HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); SecurityContext contextBeforeChainExecution = this .repo.loadContext(holder); boolean var13 = false ; try { var13 = true ; SecurityContextHolder.setContext(contextBeforeChainExecution); chain.doFilter(holder.getRequest(), holder.getResponse()); var13 = false ; } finally { if (var13) { SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); SecurityContextHolder.clearContext(); this .repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute("__spring_security_scpf_applied" ); if (debug) { this .logger.debug("SecurityContextHolder now cleared, as request processing completed" ); } } } SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext(); SecurityContextHolder.clearContext(); this .repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); request.removeAttribute("__spring_security_scpf_applied" ); if (debug) { this .logger.debug("SecurityContextHolder now cleared, as request processing completed" ); } } } }
获取认证用户信息
由前文可知,封装了已认证用户信息对象 Authentication 的 SecurityContext 即存储在 SecurityContextHolder 中,也存储在 Session 中,所以可以有两种方式获取用户信息。
☕️ 使用 SecurityContextHolder 获取
1 2 3 4 5 6 7 8 9 10 @Controller public class TestController { @GetMapping("/test1") @ResponseBody public Object test1 () { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authentication; } }
访问localhost:8080/test1
,使用 admin 的用户名和密码认证之后,浏览器页面显示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 { "authorities" : [ { "authority" : "ROLE_ADMIN" }, { "authority" : "ROLE_USER" } ], "details" : { "remoteAddress" : "0:0:0:0:0:0:0:1" , "sessionId" : null }, "authenticated" : true , "principal" : { "id" : 1 , "username" : "admin" , "password" : "$2a$10$JNVWTh5Yq56kJtrCZkcDk.DL/L/i8g3KrTAshcHW3mFf8//lnfG56" , "mobile" : "11111111111" , "roles" : "ROLE_ADMIN,ROLE_USER" , "enabled" : true , "authorities" : [ { "authority" : "ROLE_ADMIN" }, { "authority" : "ROLE_USER" } ], "accountNonExpired" : true , "credentialsNonExpired" : true , "accountNonLocked" : true }, "credentials" : null , "name" : "admin" }
由上可以验证我们前面的分析,敏感信息 credentials 被去除,principal 存储的为 UserDetails 实现类,可以通过强转获取 UserDetails 对象:
1 2 3 4 5 6 7 8 9 @GetMapping("/test2") @ResponseBody public Object test2 () { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); UserDetails user = (UserDetails) authentication.getPrincipal(); return user; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "id" : 1 , "username" : "admin" , "password" : "$2a$10$JNVWTh5Yq56kJtrCZkcDk.DL/L/i8g3KrTAshcHW3mFf8//lnfG56" , "roles" : "ROLE_ADMIN,ROLE_USER" , "mobile" : "11111111111" , "enabled" : true , "authorities" : [ { "authority" : "ROLE_ADMIN" }, { "authority" : "ROLE_USER" } ], "accountNonExpired" : true , "credentialsNonExpired" : true , "accountNonLocked" : true }
☕️ 使用 HttpSession 获取
1 2 3 4 5 6 7 8 9 @GetMapping("/test3") @ResponseBody public Object test3 (HttpSession session) { SecurityContext context = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT" ); UserDetails user = (UserDetails) context.getAuthentication().getPrincipal(); return user; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "id" : 1 , "username" : "admin" , "password" : "$2a$10$JNVWTh5Yq56kJtrCZkcDk.DL/L/i8g3KrTAshcHW3mFf8//lnfG56" , "roles" : "ROLE_ADMIN,ROLE_USER" , "mobile" : "11111111111" , "enabled" : true , "authorities" : [ { "authority" : "ROLE_ADMIN" }, { "authority" : "ROLE_USER" } ], "accountNonExpired" : true , "credentialsNonExpired" : true , "accountNonLocked" : true }
自定义认证成功/失败处理器
登录处理的方法介绍
此处先对http.formLogin()
返回值的主要方法进行说明,这些方法涉及用户登录的处理,具体如下:
loginPage(String loginPage)
:设置用户登录页面的访问路径,默认为 GET 请求的 /login
。
loginProcessingUrl(String loginProcessingUrl)
:设置登录表单提交的路径,默认为是 POST 请求的 loginPage() 设置的路径
successForwardUrl(String forwordUrl)
:设置用户认证成功后转发的地址。
successHandler(AuthenticationSuccessHandler successHandler)
:配置用户认证成功后的自定义处理器。
defaultSuccessUrl(String defaultSuccessUrl)
:设置用户认证成功后重定向的地址。这里需要注意,该路径是用户直接访问登录页面认证成功后重定向的路径,如果是其他路径跳转到登录页面认证成功后会重定向到原始访问路径。可设置第二个参数为 true,使认证成功后始终重定向到该地址。
failureForwrad(String forwardUrl)
:设置用户认证失败后转发的地址。
failureHandler(AuthenticationFailureHandler authenticationFailureHandler)
:设置用户登录失败后的自定义错误处理器。
failureUrl(String authenticationFailureUrl)
:设置用户登录失败后重定向的地址,指定的路径要能匿名访问,默认为loginPage() + ?error
。
usernameParamter(String usernameParamter)
:设置登录表单中的用户名参数,默认为 username。
passwordParamter(String passwordParamter)
:设置登录表单中的密码参数,默认为 password。
内置的处理器介绍
前面的 defaultSuccessUrl() 和 failureUrl() 方法使用的是Spring Security
内置的认证成功和失败处理器。我们也可以自定义认证成功和失败处理器,根据前端请求方式返回不同的响应类型数据,如果客户端是 ajax 请求,响应 JSON 数据通知前端认证成功或失败;如果客户端是正常的表单提交请求,认证成功时重定向到该请求的原始访问路径或指定路径,认证失败时重定向到登录页面显示错误信息。
在自定义认证成功和失败处理器之前,我们先对 defaultSuccessUrl() 和 failureUrl() 方法使用的认证和失败处理器进行介绍。
defaultSuccessUrl() 方法的处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public final T defaultSuccessUrl (String defaultSuccessUrl) { return this .defaultSuccessUrl(defaultSuccessUrl, false ); } public final T defaultSuccessUrl (String defaultSuccessUrl, boolean alwaysUse) { SavedRequestAwareAuthenticationSuccessHandler handler = new SavedRequestAwareAuthenticationSuccessHandler(); handler.setDefaultTargetUrl(defaultSuccessUrl); handler.setAlwaysUseDefaultTargetUrl(alwaysUse); this .defaultSuccessHandler = handler; return this .successHandler(handler); }
查看SavedRequestAwareAuthenticationSuccessHandler
处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { protected final Log logger = LogFactory.getLog(this .getClass()); private RequestCache requestCache = new HttpSessionRequestCache(); public SavedRequestAwareAuthenticationSuccessHandler () { } public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { SavedRequest savedRequest = this .requestCache.getRequest(request, response); if (savedRequest == null ) { super .onAuthenticationSuccess(request, response, authentication); } else { String targetUrlParameter = this .getTargetUrlParameter(); if (!this .isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) { this .clearAuthenticationAttributes(request); String targetUrl = savedRequest.getRedirectUrl(); this .logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl); this .getRedirectStrategy().sendRedirect(request, response, targetUrl); } else { this .requestCache.removeRequest(request, response); super .onAuthenticationSuccess(request, response, authentication); } } } public void setRequestCache (RequestCache requestCache) { this .requestCache = requestCache; } }
查看上述的(1)过程实现源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class HttpSessionRequestCache implements RequestCache { private String sessionAttrName; public HttpSessionRequestCache () { this .requestMatcher = AnyRequestMatcher.INSTANCE; this .sessionAttrName = "SPRING_SECURITY_SAVED_REQUEST" ; } public SavedRequest getRequest (HttpServletRequest currentRequest, HttpServletResponse response) { HttpSession session = currentRequest.getSession(false ); return session != null ? (SavedRequest)session.getAttribute(this .sessionAttrName) : null ; } }
failureUrl() 方法的处理器
1 2 3 4 5 6 public final T failureUrl (String authenticationFailureUrl) { T result = this .failureHandler(new SimpleUrlAuthenticationFailureHandler(authenticationFailureUrl)); this .failureUrl = authenticationFailureUrl; return result; }
查看SimpleUrlAuthenticationFailureHandler
处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler { private boolean forwardToDestination = false ; private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); public SimpleUrlAuthenticationFailureHandler (String defaultFailureUrl) { this .setDefaultFailureUrl(defaultFailureUrl); } public void onAuthenticationFailure (HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { if (this .defaultFailureUrl == null ) { this .logger.debug("No failure URL set, sending 401 Unauthorized error" ); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } else { this .saveException(request, exception); if (this .forwardToDestination) { this .logger.debug("Forwarding to " + this .defaultFailureUrl); request.getRequestDispatcher(this .defaultFailureUrl).forward(request, response); } else { this .logger.debug("Redirecting to " + this .defaultFailureUrl); this .redirectStrategy.sendRedirect(request, response, this .defaultFailureUrl); } } } protected final void saveException (HttpServletRequest request, AuthenticationException exception) { if (this .forwardToDestination) { request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION" , exception); } else { HttpSession session = request.getSession(false ); if (session != null || this .allowSessionCreation) { request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION" , exception); } } } }
自定义处理器
在自定义认证成功和失败处理器时,不用完全自己实现,在Spring Security
内置的认证成功和失败处理器基础上进行功能扩充即可。
☕️ 定义统一返回的 JSON 结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.example.entity;import lombok.Getter;@Getter public class ResultData < ;T> { private T data; private int code; private String msg; public ResultData () { this .code = 0 ; this .msg = "发布成功!" ; } public ResultData (int code, String msg) { this .code = code; this .msg = msg; } public ResultData (T data) { this .data = data; this .code = 0 ; this .msg = "发布成功!" ; } public ResultData (T data, String msg) { this .data = data; this .code = 0 ; this .msg = msg; } }
☕️ 自定义 jackson 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.example.config;import com.fasterxml.jackson.annotation.JsonInclude;import com.fasterxml.jackson.databind.DeserializationFeature;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.module .SimpleModule;import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;import java.text.SimpleDateFormat;@Configuration public class JacksonConfig { @Bean @Primary public ObjectMapper jacksonObjectMapper (Jackson2ObjectMapperBuilder builder) { ObjectMapper objectMapper = builder.createXmlMapper(false ).build(); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" )); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(Long.class, ToStringSerializer.instance); simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(simpleModule); return objectMapper; } }
☕️ 自定义认证成功处理器 CustomAuthenticationSuccessHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.example.config.security;import com.example.entity.ResultData;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.Authentication;import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;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 CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { String xRequestedWith = request.getHeader("x-requested-with" ); if ("XMLHttpRequest" .equals(xRequestedWith)) { response.setContentType("application/json;charset=utf-8" ); response.getWriter().write(objectMapper.writeValueAsString(new ResultData<>(0 , "认证成功!" ))); }else { super .setDefaultTargetUrl("/index" ); super .onAuthenticationSuccess(request, response, authentication); } } }
☕️ 自定义认证失败处理器 CustomAuthenticationFailureHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.example.config.security;import com.example.entity.ResultData;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;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 CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationFailure (HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { String xRequestedWith = request.getHeader("x-requested-with" ); if ("XMLHttpRequest" .equals(xRequestedWith)) { response.setContentType("application/json;charset=utf-8" ); response.getWriter().write(objectMapper.writeValueAsString(new ResultData<>(1 , "认证失败!" ))); }else { super .setDefaultFailureUrl("/login/page?error" ); super .onAuthenticationFailure(request, response, e); } } }
☕️ 修改安全配置类 SpringSecurityConfig,使用自定义认证成功和失败处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private CustomAuthenticationFailureHandler authenticationFailureHandler; @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login/page" ) .loginProcessingUrl("/login/form" ) .usernameParameter("name" ) .passwordParameter("pwd" ) .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler); } }
完整的安全配置类 SpringSecurityConfig 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package com.example.config;import com.example.config.security.CustomAuthenticationFailureHandler;import com.example.config.security.CustomAuthenticationSuccessHandler;import com.example.service.CustomUserDetailsService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;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;@EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomAuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private CustomAuthenticationFailureHandler authenticationFailureHandler; @Bean public BCryptPasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login/page" ) .loginProcessingUrl("/login/form" ) .usernameParameter("name" ) .passwordParameter("pwd" ) .successHandler(authenticationSuccessHandler) .failureHandler(authenticationFailureHandler); http.authorizeRequests() .antMatchers("/login/page" ).permitAll() .antMatchers("/admin/**" ).hasRole("ADMIN" ) .antMatchers("/user/**" ).hasAuthority("ROLE_USER" ) .anyRequest().authenticated(); http.csrf().disable(); } @Override public void configure (WebSecurity web) throws Exception { web.ignoring().antMatchers("/**/*.css" , "/**/*.js" , "/**/*.png" , "/**/*.jpg" , "/**/*.jpeg" , "/**/*.ico" ); } }
☕️ 测试
ajax 请求
使用 postman 模拟 ajax 请求进行测试:
返回的 JSON 数据如下:
1 2 3 4 { "code" : 0 , "msg" : "认证成功!" }
表单请求
访问localhost:8080/login/page
,输入错误的用户名和密码,重定向到/login/page?error
:
这里的错误信息就是前面源码分析中提到的存储在 Session 中认证错误信息,用户认证失败后,重定向到登录页面,从 Session 域中获取认证错误信息并在页面展示:
1 2 3 4 <div th:if ="${param.error}" > <span th:text ="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style ="color:red" > 用户名或密码错误</span > </div >
Spring Security
默认加载 message.properties 英文配置文件,所以显示的是英文错误提示信息。我们可以自定义配置类让Spring Security
加载官方提供的 message_zh_CN.properties 中文配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.example.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.support.ReloadableResourceBundleMessageSource;@Configuration public class ReloadZhMessageConfig { @Bean public ReloadableResourceBundleMessageSource messageSource () { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:org/springframework/security/messages_zh_CN" ); return messageSource; } }
再次访问localhost:8080/login/page
,输入错误的用户名和密码,重定向到/login/page?error
:
本文转载自:[呵呵233 ]《Spring Security 入门(一):认证和原理分析 》