补充前后端分离实现
配置登录成功
package security.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Map<String, Object> result = new HashMap<>(); result.put("msg", "登录成功!"); result.put("status", 200); response.setContentType("application/json;charset=UTF-8"); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } }
登录失败
package security.config; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; public class MyLogout implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Map<String, Object> result = new HashMap<>(); result.put("msg", "注销成功!"); result.put("status", 500); response.setContentType("application/json;charset=UTF-8"); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); }
前端Thymeleaf整合security实现
外部资源: xmlns:sec=”http://www.thymeleaf.org/extras/spring-security“
<!DOCTYPE html> <html lang="en" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns:th="http://www.thymeleaf.otg"> <head> <meta charset="UTF-8"> <title>用户界面</title> </head> <body> <h2>获取用户详细信息</h2> <ul> <li sec:authentication="principal.username"></li> <li sec:authentication="principal.authorities"></li> <li sec:authentication="principal.accountNonExpired"></li> <li sec:authentication="principal.accountNonLocked"></li> <li sec:authentication="principal.credentialsNonExpired"></li> </ul> </body> </html>
重写UserDetailsService
@Component public class MyUserDetailsService implements UserDetailsService { private final UserDao userDao; @Autowired public MyUserDetailsService(UserDao userDao) { this.userDao = userDao; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userDao.loadUserByUsername(username); if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!"); //存在的话 , 赋予权限信息 List<Role> roles = userDao.getRolesByUid(user.getId()); user.setRoles(roles); return user; } }
实现Dao层接口及其mapper
Dao
@Mapper public interface UserDao { //根据用户名查询user User loadUserByUsername(String username); //根据用户id查询角色 List<Role> getRolesByUid(Integer uid); }
mapper
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="security.dao.UserDao"> <!--查询单个loadUserByUsername--> <select id="loadUserByUsername" resultType="User"> select id, username, password, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired from user where username = #{username} </select> <!--查询指定行数据 ,表连接--> <select id="getRolesByUid" resultType="Role"> select r.id, r.name, r.name_zh nameZh from role r, user_role ur where r.id = ur.rid and ur.uid = #{uid} </select> </mapper>
多表联查新思路
用户登录数据获取 (SecurityContextHolder)
Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。
SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将SecurityContextHolder 中的数据清空。
实际上 SecurityContextHolder 中存储是 SecurityContext,在 SecurityContext 中存储是 Authentication。
1.MODE THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。
2.MODE INHERITABLETHREADLOCAL:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。
3.MODE GLOBAL:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb开发中,这种模式很少使用到。
自定义认证数据源
- 发起认证请求,请求中携带用户名、密码,该请求会被UsernamePasswordAuthenticationFilter 拦截
- 在UsernamePasswordAuthenticationFilter的attemptAuthentication方法中将请求中用户名和密码,封装为Authentication对象,并交给AuthenticationManager 进行认证
- 认证成功,将认证信息存储到 SecurityContextHodler 以及调用记住我等,并回调 AuthenticationSuccessHandler 处理
- 认证失败,清除 SecurityContextHodler 以及 记住我中信息,回调 AuthenticationFailureHandler 处理
三者关系
从上面分析中得知,AuthenticationManager 是认证的核心类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider 。他们三者关系是样的呢?
- AuthenticationManager 是一个认证管理器,它定义了 Spring Security 过滤器要执行认证操作。
- ProviderManager AuthenticationManager接口的实现类。Spring Security 认证时默认使用就是 ProviderManager。
- AuthenticationProvider 就是针对不同的身份类型执行的具体的身份认证。
Spring Seourity 中,允许系统同时支持多种不同的认证方式,例如同时支持用户名/密码认证、ReremberMe 认证、手机号码动态认证等,而不同的认证方式对应了不同的 AuthenticationProvider,所以一个完整的认证流程可能由多个 AuthenticationProvider 来提供。
多个 AuthenticationProvider 将组成一个列表,这个列表将由 ProviderManager 代理。换句话说,在ProviderManager 中存在一个 AuthenticationProvider 列表,在Provider Manager 中遍历列表中的每一个 AuthenticationProvider 去执行身份认证,最终得到认证结果。
ProviderManager 本身也可以再配置一个 AuthenticationManager 作为 parent,这样当当前ProviderManager 认证失败之后,就可以进入到 parent 中再次进行认证。理论上来说,ProviderManager 的 parent 可以是任意类型的 AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的 parent。(自己多理解几遍)
ProviderManager本身也可以有多个,多个ProviderManager 共用同一个 parent。有时,一个应用程序有受保护资源的逻辑组(例如,所有符合路径模式的网络资源,如/api/**),每个组可以有自己的专用 AuthenticationManager。通常,每个组都是一个ProviderManager,它们共享一个父级。然后,父级是一种 全局资源,作为所有提供者的后备资源。
根据上面的介绍,我们绘出新的 AuthenticationManager、ProvideManager 和 AuthentictionProvider 关系
数据源的获取。默认情况下 AuthenticationProvider 是由 DaoAuthenticationProvider 类来实现认证的,在DaoAuthenticationProvider 认证时又通过 UserDetailsService 完成数据源的校验。
总结: AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。
- 全局的AuthenticationManager用来对全x局认证进行处理
- 局部的AuthenticationManager用来对某些特殊资源认证处理。
当然无论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用 UserDetailService 来实现。
配置全局 AuthenticationManager
默认的全局 AuthenticationManager
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Autowired public void initialize(AuthenticationManagerBuilder builder) { //builder.. } }
- springboot 对 security 进行自动配置时自动在工厂中创建一个全局AuthenticationManager
总结
- 默认自动配置创建全局AuthenticationManager 默认找当前项目中是否存在自定义 UserDetailService 实例 自动将当前项目 UserDetailService 实例设置为数据源
- 默认自动配置创建全局AuthenticationManager 在工厂中使用时直接在代码中注入即可
- 自定义全局 AuthenticationManager
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override public void configure(AuthenticationManagerBuilder builder) { //builder .... } }
总结
- 一旦通过 configure 方法自定义 AuthenticationManager实现 就回将工厂中自动配置AuthenticationManager 进行覆盖
- 一旦通过 configure 方法自定义 AuthenticationManager实现 需要在实现中指定认证数据源对象 UserDetaiService 实例
- 一旦通过 configure 方法自定义 AuthenticationManager实现 这种方式创建AuthenticationManager对象工厂内部本地一个 AuthenticationManager 对象 不允许在其他自定义组件中进行注入
用来在工厂中暴露自定义AuthenticationManager 实例
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { //1.自定义AuthenticationManager 推荐 并没有在工厂中暴露出来 @Override public void configure(AuthenticationManagerBuilder builder) throws Exception { System.out.println("自定义AuthenticationManager: " + builder); builder.userDetailsService(userDetailsService()); } //作用: 用来将自定义AuthenticationManager在工厂中进行暴露,可以在任何位置注入 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
文献参考
编程不良人
深入浅出Spring Security (王松)