目录
Spring Security 学习专栏
2. Spring Security 自定义认证管理器和讲解 (二)
一、概述
上一篇Spring Security 入门学习带大家搭建一个简单Demo,认识SpringSecurity,这篇文章讲自定义认证管理器和讲解
二、自定义认证管理器
Spring Security为我们提供了一个「httpBasic」模式的简单登陆页面,并在控制台输出了密码(这个密码每次启动都是不一样的)。如果我们想用自己的定义的账号密码,则需要改配置。如下:
1. SecurityConfig
并加入一些代码,如下所示:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 认证管理器配置
* @param auth
* @date: 2021/3/11 17:39
* @return: void
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
// 使用内存中的 InMemoryUserDetailsManager
inMemoryAuthentication()
// 不使用 PasswordEncoder 密码编码器
.passwordEncoder(NoOpPasswordEncoder.getInstance())
.withUser("root").password("root").roles("ADMIN","NORMAL")
// 配置 admin 用户
.and().withUser("admin").password("admin").roles("ADMIN")
// 配置 normal 用户
.and().withUser("normal").password("normal").roles("NORMAL");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 配置请求地址的权限
.authorizeRequests()
// 所有用户可访问,不需要登入
.antMatchers("/test/echo").permitAll()
// 需要 ADMIN 角色
.antMatchers("/test/admin","/test/getUserInfo").hasRole("ADMIN")
// 需要 NORMAL 角色
.antMatchers("/test/normal","/test/getUserInfo").access("hasRole('NORMAL')") // 需要 NORMAL 角色。
// 任何请求,访问的用户都需要经过认证
.anyRequest()
.authenticated()
.and()
// 设置 Form 表单登陆
.formLogin()
//.loginPage("/login") // 登陆 URL 地址
.permitAll() // 所有用户可访问
.and().logout()
// .logoutUrl("/logout") // 退出 URL 地址
// 所有用户可访问
.permitAll()
// 禁用跨域
.and().csrf().disable();
}
}
上面的配置其实和默认情况的配置几乎一样,只是这里定义了三个用户root、admin、normal 。(说明:passwordEncoder(NoOpPasswordEncoder.getInstance()) 不使用 PasswordEncoder 密码编码器)此时我们启动项目,便可以使用root、admin、normal 都可以登入,不同的用户拥有不同角色所看的资源也不一样。
用户-角色-资源 访问控制
- root 用户可以看到所有的资源
- admin 用户可以看到 "/test/echo","/test/admin","/test/getUserInfo" 资源
- normal 用户可以看到 "/test/echo","/test/normal","/test/getUserInfo" 资源
2. TestController
@Slf4j
@RestController
@RequestMapping(value = "/test")
public class TestController {
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
@GetMapping("/getUserInfo")
public String getUserInfo(){
String currentUser ;
Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principl instanceof UserDetails) {
currentUser = ((UserDetails)principl).getUsername();
}else {
currentUser = principl.toString();
}
log.info("getUserInfo.resp currentUser is:{}",currentUser);
return " currentUser is ===>: " +currentUser;
}
}
3. 测试验证
下面来验证一下普通用户登录,重启项目,在浏览器中输入:http://127.0.0.1:8080/test/admin。同样,我们会到达登录页面,我们输入用户名 normal,密码也为normal结果错误页面了,拒绝访问了,信息为:
我们把浏览器中的uri修改成:/test/normal,结果访问成功。可以看到 我是普通用户。说明 normal这个USER角色只能访问 "/test/echo","/test/normal","/test/userInfo" ,这个结果与我们预期一致。
再来验证一下超级管理员用户登录,重启浏览器之后,输入http://127.0.0.1:8080/test/admin。在登录页面中输入用户名root,密码root,提交之后,可以看到 我是管理员
,说明访问管理员资源了。我们再将浏览器uri修改成 /test/normal,刷新之后,也能看到我是普通用户,说明 root用户可以访问所有资源,这个也和我们的预期一致。
4. 获取当前登录用户信息
上面我们实现了“资源 - 角色”的访问控制,效果和我们预期的一致,但是并不直观,我们不妨尝试在控制器中获取“当前登录用户”的信息,直接输出,看看效果。
以/test/userInfo为例,我们修改其代码,如下:
@GetMapping("/getUserInfo")
public String getUserInfo(){
String currentUser ;
Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principl instanceof UserDetails) {
currentUser = ((UserDetails)principl).getUsername();
}else {
currentUser = principl.toString();
}
log.info("getUserInfo.resp currentUser is:{}",currentUser);
return " currentUser is ===>: " +currentUser;
}
这里,我们通过SecurityContextHolder
来获取了用户信息,并拼接成字符串输出。重启项目,在浏览器访问http://127.0.0.1:8080/test/getUserInfo. 使用 admin的身份登入,可以看到浏览器显示 currentUser is: admin
。使用 root 的身份登入,可以看到浏览器显示 currentUser is: root
。
小结
至此,我们已经对Spring Security有了一个基本的认识了。了解了如何在项目中加入spring security,以及如何控制资源的角色访问控制。spring security原不止这么简单,我们才刚刚开始。为了能够更好的在实战中使用spring security 我们需要更深入的了解。下面我们先来了解spring security的一些核心概念。
三、Spring Security 核心组件
Spring Security 核心组件有:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager,下面分别介绍。
1. SecurityContext
安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中,SecurityContext的接口定义如下:
public interface SecurityContext extends Serializable {
/**
* Obtains the currently authenticated principal, or an authentication request token.
*
* @return the <code>Authentication</code> or <code>null</code> if no authentication
* information is available
*/
Authentication getAuthentication();
/**
* Changes the currently authenticated principal, or removes the authentication
* information.
*
* @param authentication the new <code>Authentication</code> token, or
* <code>null</code> if no further authentication information should be stored
*/
void setAuthentication(Authentication authentication);
}
可以看到 SecurityContext 接口只定义了两个方法,实际上其主要作用就是获取 Authentication 对象。
2. SecurityContextHolder
SecurityContextHolder 看名知义,是一个holder,用来hold住SecurityContext实例的。在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession属性。它会为每个请求恢复上下文SecurityContextHolder,并且最重要的是,在请求完成时清除SecurityContextHolder。SecurityContextHolder是一个类,他的功能方法都是静态的(static)。
SecurityContextHolder可以设置指定JVM策略(SecurityContext的存储策略),这个策略有三种:
- MODE_THREADLOCAL:SecurityContext 存储在线程中。
- MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
- MODE_GLOBAL:SecurityContext 在所有线程中都相同。
SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。在spring security应用中,我们通常能看到类似如下的代码:
SecurityContextHolder.getContext().setAuthentication(token);
其作用就是存储当前认证信息。
3. Authentication
authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。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;
}
接口有4个get方法,分别获取
- Authorities, 填充的是用户角色信息。
- Credentials,直译,证书。填充的是密码。
- Details ,用户信息。
- Principal 直译,形容词是“主要的,最重要的”,名词是“负责人,资本,本金”。感觉很别扭,所以,还是不翻译了,直接用原词principal来表示这个概念,其填充的是用户名。
因此可以推断其实现类有这4个属性。这几个方法作用如下:
- getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
- getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
- getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)
- getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。
- isAuthenticated: 获取当前 Authentication 是否已认证。
- setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。
4. UserDetails
UserDetails,看命知义,是用户信息的意思。其存储的就是用户信息,其定义如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
方法含义如下:
- getAuthorites:获取用户权限,本质上是用户的角色信息。
- getPassword: 获取密码。
- getUserName: 获取用户名。
- isAccountNonExpired: 账户是否过期。
- isAccountNonLocked: 账户是否被锁定。
- isCredentialsNonExpired: 密码是否过期。
- isEnabled: 账户是否可用。
5. UserDetailsService
提到了UserDetails就必须得提到UserDetailsService, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername,他可以用来获取UserDetails。
通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。
在实现loadUserByUsername方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException。
6. AuthenticationManager
AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException 异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。
项目代码
https://gitee.com/gaibianzlp/spring-security-demo.git
小结
这里,我们只是简单的了解了spring security中有哪些东西,先混个脸熟。这里并不需要我们一下子全记住这些名词和概念。先大概看看,有个印象。