在spring-security官网中认证是由AuthenticationManager
接口来进行负责的,定义为
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
官方文档解释 : 尝试对传递 Authentication 的对象进行身份验证,如果成功,则返回完全填充 Authentication 的对象(包括授予的权限)。 必须 AuthenticationManager 履行以下有关例外情况的合同: 如果帐户被禁用AuthenticationManager,则必须抛出 ,DisabledException并且可以测试此状态。 如果帐户被锁定AuthenticationManager,则必须抛出 aLockedException,并且可以测试帐户锁定。 如果提供不正确的凭据,则必须抛出 。 BadCredentialsException 虽然上述例外是可选AuthenticationManager 但必须 始终 测试凭据。 应测试异常,如果适用,应按上述顺序抛出(即,如果帐户被禁用或锁定,则身份验证请求将立即被拒绝,并且不执行凭据测试过程)。这可以防止针对已禁用或锁定的帐户测试凭据。 形参: 身份验证 – 身份验证请求对象 返回值: 经过完全身份验证的对象,包括凭据 抛出: AuthenticationException – 如果身份验证失败
从官方文档我们就可以了解出: 如果
Authentication
表示认证成功AuthenticationException
异常,表示认证失败。AuthenticationManager
的主要实现类为 ProviderManager
在 ProviderManager
中管理了众多 AuthenticationProvider
实例。在一次完整的认证流程中,Spring Security 允许存在多个 AuthenticationProvider
,用来实现多种认证方式,这些 AuthenticationProvider
都是由 ProviderManager
进行统一管理的
认证以及认证成功的信息主要是由 Authentication 的实现类进行保存的,其接口定义为:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
getAuthorities
获取用户权限信息getCredentials
获取用户凭证信息,一般指密码getDetails
获取用户详细信息getPrincipal
获取用户身份信息,用户名、用户对象等isAuthenticated
用户是否认证成功它通过实现类封装了我们需要的用户的信息,我们则是通过
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
System.out.println("获取username:" + user.getUsername());
System.out.println("获取password:" + user.getPassword());
来实现对用户信息的获取。
SecurityContextHolder 用来获取登录之后用户信息。Spring Security 会将登录用户数据保存在 Session 中。
好处: 方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据
以上就是在安全认证时,最重要的几个接口
web和security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
数据库和mybatis依赖
<!--数据库的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.7</version>
</dependency>
thymeleaf 和 security 联合依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- springSecurity 和前端的交互-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
# 设置thymeleaf的缓存
spring.thymeleaf.cache=false
# datasource
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ssm?characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
# mybatis
mybatis.mapper-locations=classpath:security/mapper/*.xml
mybatis.type-aliases-package=security.pojo
# log为了展现mybatis运行 sql 语句
logging.level.com.security=debug
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/loginPages").setViewName("loginPage");
registry.addViewController("/index").setViewName("index");
}
}
主要配置了常用的公共视图跳转资源的接口,减少了controller层的代码量
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//配置资源...
}
覆盖此方法以配置 WebSecurity。 例如,如果您希望忽略某些请求。Spring Security将忽略此方法中指定的端点,这意味着它不会保护它们免受CSRF,XSS,点击劫持等的侵害。相反,如果要保护终结点免受常见漏洞的影响,请参阅 configure(HttpSecurity) 和 HttpSecurity.authorizeRequests 配置方法。
重写此方法以配置 HttpSecurity.通常,子类不应通过调用 super 来调用此方法,因为它可能会覆盖其配置。默认配置为: http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic(); 可以在此处指定任何需要防御常见漏洞的终结点,包括公共终结点。有关公共终结点的更多详细信息,请参阅 HttpSecurity.authorizeRequests 和“permitAll()”授权规则。 形参: HTTP – HttpSecurity 要修改的 抛出: Exception – 如果发生错误
//认证
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/index").permitAll()
.mvcMatchers("/loginPages").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/loginPages")
.loginProcessingUrl("/doLogin")
.usernameParameter("username")
.passwordParameter("password")
.defaultSuccessUrl("/index")
//.successForwardUrl("/index")
//.failureUrl("/loginPages")
// .failureForwardUrl("/loginPages")
//.successHandler(new MyAuthenticationSuccessHandler()) //前后端分离的处理方式
.failureHandler(new FailureHandler())
.and()
.logout()
.invalidateHttpSession(true) //默认删除session会话
.clearAuthentication(true) // 默认清楚认证标记
.logoutSuccessUrl("/loginPages")
.logoutSuccessHandler(new MyLogout()) //前后端分离注销成功的处理
.and()
.csrf().disable();
}
.and()
.logout()
.invalidateHttpSession(true) //默认删除session会话
.clearAuthentication(true) // 默认清楚认证标记
.logoutSuccessUrl("/loginPages")
.logoutSuccessHandler(new MyLogout()) //前后端分离注销成功的处理
.and()
/logout
.loginProcessingUrl("/doLogin")
.usernameParameter("username")
.passwordParameter("password")
一般来讲,我们用户完成输入,表单进行提交时都需要与security中的方法进行匹配
默认的loginProcessingUrl为login
、 usernameParameter 为 username
、passwordParameter 为 password
因此我们可以通过修改其中的值,来匹配我们自己的接口,及其属性value
* .anyRequest().authenticated()
* 下面的所有请求都是需要认证之后的
*
* .formLogin()
* 开启表单认证(value为登录页面)
*
* .formLogin().loginPage("/loginPages")
* 用于覆盖默认的登录页面 ,“/loginPages”为一个请求接口
*
* .loginProcessingUrl("/login")
* 用来处理登录请求的url
* .defaultSuccessUrl("/index")
* 默认成功地址, 是一个重定向,
* 比如之前打开/hello请求,但是它跳转到了login,登录完成后
* 如果使用的是defaultSuccessUrl ,那么他依然会跳转至/hello请求
* .successForwardUrl("/index")
* 成功跳转路径, 始终跳转到指定的请求, 比如之前打开/hello请求,但是它跳转到了login,登录完成后
* 如果使用的是上面这个successForwardUrl,那么他就会优先跳转至指定的index请求,而不是/hello
*
* .failureForwardUrl("/toLogin")
* 登录失败跳转路径 ,返回的错误信息是在request作用域中
* 展示错误信息 :th:text="${SPRING_SECURITY_LAST_EXCEPTION}">
*
* .failureUrl()
* 登录失败跳转路径 ,返回的错误信息是在session作用域中
* 展示错误信息 th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}">
* .and().csrf().disable();
* 关闭跨站请求保护,为了测试
一般来讲 , .anyRequest().authenticated()
后面配置的资源/请求 , 他们都是需要实现认证才能被访问的
所以,我们通常将公共资源放置他之前, 然后用permitAll() 来过滤
.mvcMatchers("/index").permitAll()
.mvcMatchers("/loginPages").permitAll()
作用: 设置应用上下文
如果我们想要自己设置userDetailsService认证 , 我们就可以通过重写protected void configure(AuthenticationManagerBuilder builder)
方法
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userDetailsService);
}
来设置我们自己定义的service接口,然后注入到容器中,等待被调用
重写此方法以将 AuthenticationManager 要公开的 from configure(AuthenticationManagerBuilder) 作为 Bean 公开。例如: @Bean(name name=”myAuthenticationManager”) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } 返回值: 这 AuthenticationManager 抛出: Exception
//将自定义的认证暴露在工厂中 (加入到容器中去管理)
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
重写自己的userDetailsService()然后通过上面的authenticationManagerBean
,将自己重写的serivce注入到容器中,作为公开的bean
重写此方法以将 UserDetailsService 创建自 configure(AuthenticationManagerBuilder) 公开为 Bean。通常,此方法只应执行以下覆盖: @Bean(name = “myUserDetailsService”) // any or no name specified is allowed @Override public UserDetailsService userDetailsServiceBean() throws Exception { return super.userDetailsServiceBean(); } 要更改返回的实例,开发人员应改为更改userDetailsService() 返回值: 这 UserDetailsService 抛出: Exception – 请参阅: userDetailsService()
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);
}
外部资源: 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
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>
多表联查新思路
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。
MODE THREADLOCAL
:这种存放策略是将 SecurityContext 存放在 ThreadLocal中,大家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其实非常适合 web 应用,因为在默认情况下,一个请求无论经过多少 Filter 到达 Servlet,都是由一个线程来处理的。这也是 SecurityContextHolder 的默认存储策略,这种存储策略意味着如果在具体的业务处理代码中,开启了子线程,在子线程中去获取登录用户数据,就会获取不到。MODE INHERITABLETHREADLOCAL
:这种存储模式适用于多线程环境,如果希望在子线程中也能够获取到登录用户数据,那么可以使用这种存储模式。MODE GLOBAL
:这种存储模式实际上是将数据保存在一个静态变量中,在 JavaWeb开发中,这种模式很少使用到。UsernamePasswordAuthenticationFilter
拦截UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法中将请求中用户名和密码,封装为Authentication
对象,并交给AuthenticationManager
进行认证AuthenticationSuccessHandler
处理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。
当然无论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用 UserDetailService 来实现。
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
public void initialize(AuthenticationManagerBuilder builder) {
//builder..
}
}
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder builder) {
//builder ....
}
}
用来在工厂中暴露自定义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();
}
}