Spring Security是一种基于Spring AOP和Servlet过滤器Filter的安全框架,它提供全面的安全性解决方案,提供在Web请求和方法调用级别的用户鉴权和权限控制。图10-1展示了Filter在一个HTTP请求过程中的执行流程。
Web应用的安全性通常包括:用户认证(Authentication)和用户授权(Authorization)两个部分。分别阐述如下:
❑用户认证:指的是验证某个用户是否为系统合法用户,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
❑用户授权:指的是验证某个用户是否有权限执行某个操作。
Spring Security提供了多种登录认证策略。例如典型的基于表单登录认证的流程如图。
Spring Security基于表单的登录认证流程
介绍其中涉及的核心组件类。
1. SecurityContextHolder
SecurityContextHolder用于存储安全上下文(Security Context)的信息。例如:当前操作的用户对象信息、认证状态、角色权限信息等,这些都保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocalSecurityContextHolderStrategy类来存储认证信息。这个类定义如下(final类型不可变):
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal contextHolder = new ThreadLocal();
…
}
其中我们看到static final ThreadLocal
2.获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。典型的获取当前登录用户姓名的例子,使用Java代码如下所示:
Object principal = SecurityContextHolder.getContext().getAuthentication(). getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
其中,getAuthentication()返回认证信息,getPrincipal()返回身份信息,UserDetails是Spring Security对用户信息封装类。
3. Authentication
Authentication(认证信息接口)是org.springframework.security.core包中的接口,继承自Principal类。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;
}
接口中的方法如下:
其中,Principal是位于java.security包中的接口。通过Authentication接口,我们可以得到用户拥有的权限信息列表、密码、用户细节信息、用户身份信息、认证信息等。一个典型的Authentication数据示例如下:
{
"SPRING_SECURITY_CONTEXT": {
"authentication": {
"authorities": [
{
"authority": "ROLE_ADMIN"
},
{
"authority": "ROLE_USER"
}
],
"details": {
"remoteAddress": "127.0.0.1",
"sessionId": "F673D514413390BADE93ED21FF16A4A2"
},
"authenticated": true,
"principal": {
"password": null,
"username": "admin",
"authorities": [
{
"authority": "ROLE_ADMIN"
},
{
"authority": "ROLE_USER"
}
]
}
4. AuthenticationManager
AuthenticationManager(认证管理器)负责验证。认证成功后,AuthenticationManager返回一个填充了用户认证信息(包括上面提到的权限信息、身份信息、细节信息等,但密码通常会被移除)的Authentication实例。SecurityContextHolder安全上下文容器将填充了信息的Authentication,通过如下方法
SecurityContextHolder.getContext().setAuthentication()
设置到SecurityContextHolder容器中。
AuthenticationManager接口是认证相关的核心接口,也是发起认证的入口。Authentication-Manager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager内部会维护一个List
下面是ProviderManager认证部分的关键源码:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// 维护一个AuthenticationProvider列表
private List providers = Collections.emptyList();
// 认证逻辑
public Authentication authenticate(Authentication authentication)throws AuthenticationException {
Class extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
// 遍历Providers列表依次认证
for (AuthenticationProvider provider : getProviders()) {
if (! provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result ! = null) {
copyDetails(authentication, result);
break;
}
}
..
catch (AuthenticationException e) {
lastException = e;
}
}
// 如果有Authentication信息,则直接返回
if (result ! = null) {
if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
//移除密码
((CredentialsContainer) result).eraseCredentials();
}
//发布登录成功事件
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
..
// 如果没有认证成功,包装异常信息
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
// 抛出异常
throw lastException;
}
}
ProviderManager中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager会抛出一个ProviderNotFoundException异常。
到这里,我们可以简单小结下:身份信息Authentication存放在容器SecurityContext-Holder中,身份认证管理器AuthenticationManager负责管理认证流程。真正进行认证的逻辑由AuthenticationProvider接口的具体实现提供。下面介绍最为常用的DaoAuthenticationProvider。
5. DaoAuthenticationProvider
AuthenticationProvider(基于数据库的认证器)最常用的一个实现是DaoAuthentication-Provider。顾名思义,Dao正是数据访问层的缩写。用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名所提交的密码和数据库中保存的密码是否相同。DaoAuthenticationProvider类的核心代码如下:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider{
protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
…
return loadedUser;
}
..
}
retrieveUser()方法返回一个UserDetails对象。在Spring Security中提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken对象,而根据用户名加载用户的任务则是交给了UserDetailsService去做。我们只需要实现一个UserDetailsService的接口实现即可完成基于数据库的用户名密码登录认证。
UsernamePasswordAuthenticationToken和UserDetails密码的比对,由additionalAuthentication-Checks方法完成:
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {
…
String presentedPassword = authentication.getCredentials().toString();
if (! passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
}
}
如果这个void additionalAuthenticationChecks()方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder。
6. UserDetailsService
用户相关的信息是通过如下接口加载:
org.springframework.security.core.userdetails.UserDetailsService
该接口的唯一方法是:
loadUserByUsername(String username)
用来根据用户名加载相关的信息。这个方法的返回值是如下:
org.springframework.security.core.userdetails.UserDetails
其中包含了用户的信息,包括用户名、密码、权限、是否启用、是否被锁定、是否过期等。UserDetails这个接口代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段。
UserDetails接口协议如下:
public interface UserDetails extends Serializable {
Collection extends GrantedAuthority>getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
其中最重要的是用户权限getAuthorities(),由如下接口表示:
org.springframework.security.core.GrantedAuthority
UserDetails接口和Authentication接口很类似,比如它们都拥有username、authorities,两者对比参见如下。
不过,Authentication的getCredentials()与UserDetails中的getPassword()是不同的。getCredentials()是用户提交的密码凭证,getPassword()是用户正确的密码。认证器其实就是对这两者的比对。其中,UserDetailsService接口定义如下:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息。UserDetails-Service常见的实现类有从数据库加载用户信息的JdbcDaoImpl和从内存中加载用户信息的InMemoryUserDetailsManager等。当然,我们也可以自己实现UserDetailsService,在后面的项目实战的例子中,我们将会采用自己实现UserDetailsService接口的方式。
实战
pom.xml中引入
org.springframework.security
spring-security-web
org.springframework.security
spring-security-config
org.thymeleaf.extras
thymeleaf-extras-springsecurity4
security默认的用户名是user,默认密码是应用启动的时候,通过UUID算法随机生成;如果提供了passwordEncoder,使用passwordEncoder进行加密。
输入用户名、密码,点击登录(Login),浏览器返回信息后我们可以看到系统已经登录成功,只不过一个测试页面都没有,直接进入了一个默认的Error Page。我们可以写一个获取当前登录用户认证信息的HTTP接口来测试一下。代码如下:
public class User implements UserDetails, CredentialsContainer {
private String password;
private final String username;
private final Set authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
}
@RestController
class CurrentAuthentation() {
@GetMapping(value = ["", "/"])
fun auth(): Authentication? {
val request = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request
// 从Spring Security当前session中获取SPRING_SECURITY_CONTEXT
val authentication = (request.session.getAttribute("SPRING_SECURITY_CONTEXT") as SecurityContext).authentication
return authentication
}
}
重启应用,从控制台中找到密码,输入用户名、密码登录。登录成功后,可以发现输出结果如下:
{
"authorities": [
],
"details": {
"remoteAddress": "127.0.0.1",
"sessionId": "67CDDBBA47AFE55C532E6897A00C6FF0"
},
"authenticated": true,
"principal": {
"password": null,
"username": "user",
"authorities": [
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true,
"name": "user"
},
"credentials": null,
"name": "user"
}
另外,在控制台中我们可以看到DefaultSecurityFilterChain如下:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.www.BasicAuthentica-tionFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthen-ticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecuri-tyInterceptor
这些过滤器是Spring Security实现安全权限的核心。
当然这只是一个初级的配置,更复杂的配置可以分不同角色,在控制范围上能够拦截到方法级别的权限控制。
Spring Security表达式
Spring Security提供了Spring EL表达式,允许我们在定义URL路径访问@Request-Mapping的方法上面添加注解,来控制访问权限。
在标注访问权限时,根据对应的表达式返回结果,控制访问权限。Spring Security表达式操作对象的标准接口是SecurityExpressionOperations。Security提供的实现类是Security-ExpressionRoot,例如,其中的代码片段如下:
public abstract class SecurityExpressionRoot implements SecurityExpression-
Operations { ...
public final boolean hasAuthority(String authority) {
return hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
public final boolean hasRole(String role) {
return hasAnyRole(role);
}
public final boolean hasAnyRole(String... roles) {
return hasAnyAuthorityName(defaultRolePrefix, roles);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
}
..
}
通过源码,我们可以看出hasRole背后调用的是hasAnyRole, hasAnyRole调用了hasA nyAuthorityName(defaultRolePrefix, roles)。而且Role的默认前缀是:
private String defaultRolePrefix = "ROLE_";
可见,我们在学习一个框架的时候,最好的方法就是阅读源码。通过源码,我们可以更深入地理解技术的本质。SecurityExpressionRoot为我们提供的使用Spring EL表达式参见如下。
2.权限控制实例
下面我们来实现表10-4中的权限控制。
表10-4 权限控制说明
完整的实现代码在CurrentAuthentationController.kt中。关键代码如下:
@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.and().logout().logoutSuccessUrl("/login")
}
@Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
val passwordEncoder = passwordEncoder()
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)
.withUser("user").roles("USER")
.password(passwordEncoder.encode("user")) // 存放的密码需要通过 encode 加密
.and()
.withUser("admin").roles("ADMIN", "USER")
.password(passwordEncoder.encode("admin"))
}
/**
* 密码加密算法
*
* @return
*/
@Bean
open fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder();
}
}
// 使用注解 @EnableGlobalMethodSecurity 开启 Spring Security 方法级安全
/**
prePostEnabled :决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
secureEnabled : 决定是否Spring Security的保障注解 [@Secured] 是否可用
jsr250Enabled :决定 JSR-250 annotations 注解[@RolesAllowed,..] 是否可用.
*/
代码说明如下。(1)处在方法上添加@PreAuthorize这个注解,value="hasRole('ADMIN')")是Spring-EL expression。当表达式值为true,标识这个方法可以被调用;如果表达式值是false,标识此方法无权限访问。其中的hasAuthority('ROLE_ADMIN')等价于hasRole('ADMIN'),表示该方法只有当用户拥有ADMIN角色的时候才允许访问。
为了使上面的安全认证注解生效,还需要在WebSecurityConfig类上面添加@EnableWeb-Security和@EnableGlobalMethodSecurity(prePostEnabled = true)注解:
@Conguration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
...
}
这里使用注解@EnableGlobalMethodSecurity开启Spring Security方法级安全,prePost-Enabled = true决定Spring Security的@PreAuthorize, @PostAuthorize等注解是否可用。proxy-TargetClass = true表示使用基于子类代理(subclass-based CGLIB)替换基于接口(Java interface-based)的代理。默认值是false。
对应的WebSecurityConfig配置代码如下:
@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.and().logout().logoutSuccessUrl("/login")
}
@Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
val passwordEncoder = passwordEncoder()
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)
.withUser("user").roles("USER")
.password(passwordEncoder.encode("user")) // 存放的密码需要通过encode
// 加密
.and()
.withUser("admin").roles("ADMIN", "USER")
.password(passwordEncoder.encode("admin"))
}
/**
* 密码加密算法
*
* @return
*/
@Bean
open fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder();
}
在开发环境,我们可以开启WebSecurity的debug日志:@EnableWebSecurity-(debug =true),方便定位问题。从日志中我们可以看到更多关于Spring Security内部运行的信息。
3.运行测试
使用user用户登录(没有ADMIN角色),访问:http://127.0.0.1:8080/auth,页面提示鉴权失败,禁止访问,
源码:https://github.com/kaysanshi/demo_security_simple
以上是书中作者的介绍,对于真正的使用这么一个技术,我还没有使用过,不过我相信在未来的时日自己要做一个demo把这个里面介绍的知识给融汇贯通以下,在其中发挥出它优势