本章首先介绍如何使用Spring Security创建独立验证的管理员权限系统、会员系统,讲解如 何进行分表、分权限' 分登录入口、分认证接口、多注册接口,以及RBAC权限的设计和实现,如何使用JWT为手机APP提供token认证;
然后讲解Apache的Shiro安全框架的基本理论基础, 以及如何使用Shiro构建完整的用户权限系统;
最后对比分析Spring Security和Shiro的区别。
10.1.1 认识 Spring Security
Spring Security提供了声明式的安全访问控制解决方案(仅支持基于Spring的应用程序),对 访问权限进行认证和授权,它基于Spring AOP和Servlet过滤器,提供了安全性方面的全面解决 方案。
除常规的认证和授权外,它还提供了 ACLs、LDAP、JAAS、CAS等高级特性以满足复杂环 境下的安全需求。
1.核心概念
Spring Security的3个核心概念。
在Spring Security中,Authority和Permission是两个完全独立的概念,两者并没有必然的 联系。它们之间需要通过配置进行关联,可以是自己定义的各种关系。
2.认证和授权
安全主要分为验证(authentication)和授权(authorization )两个部分。
(1) 验证 (authentication)
验证指的是,建立系统使用者信息(Principal)的过程。使用者可以是一个用户、设备,和可以 在应用程序中执行某种操作的其他系统。
用户认证一般要求用户提供用户名和密码,系统通过校验用户名和密码的正确性来完成认证的 通过或拒绝过程。
Spring Security支持主流的认证方式,包括HTTP基本认证、 HTTP表单验证、HTTP摘要认证、Open ID和LDAP等。
Spring Security进行验证的步骤如下。
① 用户使用用户名和密码登录。
② 过滤器(UsemamePasswordAuthenticationFilter)获取到用户名、密码,然后封装成 Authentication o
③ Authentication Manager 认证 token (Authentication 的实现类传递)。
④ AuthenticationManager认证成功,返回一个封装了用户权限信息的Authentication对象, 用户的上下文信息(角色列表等)。
⑤ Authentication对象赋值给当前的SecurityContext,建立这个用户的安全上下文(通过调 用 SecurityContextHolder.getContext().setAuthentication())。
⑥ 用户进行一些受到访问控制机制保护的操作,访问控制机制会依据当前安全上下文信息检查 这个操作所需的权限。
除利用提供的认证外,还可以编写自己的Filter(过滤器), 提供与那些不是基于Spring Security 的验证系统的操作。
(2)授权(authorization)
在一个系统中,不同用户具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
它判断某个Principal在应用程序中是否允许执行某个操作。在进行授权判断之前,要求其所要 使用到的规则必须在验证过程中已经建立好了;
对Web资源的保护,最好的办法是使用过滤器。对方法调用的保护,最好的办法是使用AOP
Spring Security在进行用户认证及授予权限时,也是通过各种拦截器和AOP来控制权限访问 的,从而实现安全。
3.模块
10.1.2核心类
1、Securitycontext
Securitycontext中包含当前正在访问系统的用户的详细信息,它只有以下两种方法。
SecurityContext 的信息是由 SecurityContextHolder来处理的。
2、SecurityContextHolder
SecurityContextHolder 用来保存 SecurityContext。最常用的是 getContext()方法,用来获得当前 SecurityContext
SecurityContextHolder中定义了一系列的静态方法,而这些静态方法的内部逻辑是通过 SecurityContextHolder 持有的 SecurityContextHolderStrategy实现的,如 clearContext()、 getContext ()、setContext()、createEmptyContext();
SecurityContextHolderStrategy 的关键代码如下:
public interface SecurityContextHolderStrategy {
void clearContext();
Securitycontext getContext();
void setContext(SecurityContext context);
Securitycontext createEmptyContext();
}
(1) strategy 实现。
默认使用的 strategy 就是基于ThreadLocal 的 ThreadLocalSecurityContextHolderStrategy 来实现的。
除了上述提到的,Spring Security还提供了 3种类型的strategy来实现。
—般情况下,使用默认的strategy即可。但是,如果要改变默认的strategy, Spring Security提供了两种方法来改变“strategyName”。
SecurityContextHolder 类中有 3 种不同类型的 strategy, 分别为 MODE_THREADLOCAL、 MODE_INHERITABLETHREADLOCAL和 MODE_GLOBAL,
关键代码如下:
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODEJNHERITABLETHREADLOCAL = "MODE_JNHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
MODE_THREADLOCAL是默认的方法。
如果要改变strategy, 则有下面两种方法:
(2) 获取当前用户的SecurityContext
Spring Security使用一个Authentication对象来描述当前用户的相关信息。SecurityContextHolder中持有的是当前用户的Securitycontext,而Securitycontext持有的是代表当前用户相关信息的Authentication的引用。
这个Authentication对象不需要自己创建,Spring Security会自动创建相应的Authentication 对象,然后赋值给当前的SecurityContexto但是,往往需要在程序中获取当前用户的相关信息,
比如最常见的是获取当前登录用户的用户名。在程序的任何地方,可以通过如下方式获取到当前用 户的用户名。
public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPnncipal();
if (principal instanceof UserDetails){
return ((UserDetails) principal).getUsemame();
}
if (principal instanceof Principal) {
return ((Principal) principal).getName();
}
return String.valueOf(principal);
}
getAuthentication()方法会返回认证信息。
getPrincipalQ方法返回身份信息,它是UserDetails对身份信息的封装。
获取当前用户的用户名,最简单的方式如下:
public String getCurrentUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
在调用 SecurityContextHolder.getContext()获取 Securitycontext 时,如果对应的 Securitycontext 不存在,则返回空的 SecurityContext
3、ProviderManager
ProviderManager会维护一个认证的列表,以便处理不同认证方式的认证,因为系统可能会存 在多种认证方式,比如手机号、用户名密码、邮箱方式。
在认证时,如果ProviderManager的认证结果不是null,则说明认证成功,不再进行其他方 式的认证,并且作为认证的结果保存在SecurityContext中。如果不成功,则抛出错误信息 "ProviderNotFoundException"
4、DaoAuthenticationProvider
它是Authenticationprovider最常用的实现,用来获取用户提交的用户名和密码,并进行正确 性比对。如果正确,则返回一个数据库中的用户信息。
当用户在前台提交了用户名和密码后,就会被封装成UsemamePasswordAuthenticationToken。
然后,DaoAuthenticationProvider 根据 retrieveUser 方法,交给 additionalAuthenticationChecks 方法完成 UsemamePasswordAuthenticationToken 和 UserDetails 密码的比对。
如果 这个方法没有抛出异常,则认为比对成功。
比对密码需要用到PasswordEncoder和SaltSource
5、UserDetails
UserDetails是Spring Security的用户实体类,包含用户名、密码、权限等信息。Spring Security默认实现了内置的User类,供Spring Security安全认证使用。
当然,也可以自己实现。
UserDetails 接口和 Authentication 接口很类似,都拥有 username 和 authorities。一定要 区分清楚 Authentication 的 getCredentials()与 UserDetails 中的 getPassword();
前者是用户 提交的密码凭证,不一定是正确的,或数据库不一定存在;后者是用户正确的密码,认证器要进行 比对的就是两者是否相同。
Authentication 中的 getAuthorities()方法是由 UserDetails 的 getAuthorities()传递而形成 的。UserDetails的用户信息是经过Authenticationprovider认证之后被填充的。
UserDetails中提供了以下几种方法。
6、UserDetailsService
用户相关的信息是通过UserDetailsService接口来加载的。该接口的唯一方法是 loadUserByUsername(String username),用来根据用户名加载相关信息。
这个方法的返回值是 UserDetails接口,其中包含了用户的信息,包括用户名、密码、权限、是否启用、是否被锁定、 是否过期等。
7、GrantedAuthority
GrantedAuthority中只定义了一个getAutho「ity()方法。该方法返回一个字符串,表示对应权 限的字符串。如果对应权限不能用字符串表示,则返回null;
GrantedAuthority 接口通过 UserDetailsService 进行加载,然后赋予 UserDetails;
Authentication的getAuthorities()方法可以返回当前Authentication对象拥有的权限,其返 回值是一个GrantedAuthority类型的数组。每一个GrantedAuthority对象代表赋予当前用户的一 种权限;
8、Filter
(1 ) SecurityContextPersistenceFilter
它从SecurityContextRepository中取岀用户认证信息。为了提高效率,避免每次请求都要查 询认证信息,它会从Session中取出已认证的用户信息,然后将其放入SecurityContextHolder 中,以便其他Filter使用。
(2) WebAsyncManagerlntegrationFilter
集成了 SecurityContext 和 WebAsyncManager,把 SecurityContext 设置到异步线程,使 其也能获取到用户上下文认证信息。
(3) HanderWriterFilter
它对请求的Header添加相应的信息。
(4) CsrfFilter
跨域请求伪造过滤器。通过客户端传过来的token与服务器端存储的token进行对比,来判断 请求的合法性。
(5) LogoutFilter
匹配登出URL;匹配成功后,退岀用户,并清除认证信息。
(6) UsernamePasswordAuthenticationFilter
登录认证过滤器,默认是对 "/login" 的POST请求进行认证。该方法会调用attemptAuthentication, 尝试获取一个Authentication认证对象,以保存认证信息,
然后转向下一个Filter,最后调用 successfulAuthentication 执行认证后的事件。
(7) AnonymousAuthenticationFllter
如果SecurityContextHolder中的认证信息为空,则会创建一个匿名用户到SecurityContextHolder 中;
(8) SessionManagementFilter
持久化登录的用户信息。用户信息会被保存到Session、Cookie,或Redis中。
10.2.1 继承 WebSecurityConfigurerAdapter
通过重写抽象接口 WebSecurityConfigurerAdapter,再加上注解@EnableWebSecurity, 可以实现Web的安全配置。
WebSecurityConfigurerAdapter Config 模块一共有 3 个 builder (构造程序)。
配置安全,通常要重写以下方法:
//通过auth对象的方法添加身份验证
protected void configure(AuthenticationManagerBuilder auth) throws Exception {}
//通常用于设置忽略权限的静态资源
public void configure(WebSecurity web) throws Exception {}
//通过HTTP对象的authorizeRequests()方法定义URL访问权限。默认为formLogin()提供一个简单的登录验证页面
protected void configure(HttpSecurity httpSecurity) throws Exception {}
10.2.2配置自定义策略
配置安全需要继承WebSecurityConfigurerAdapter,然后重写其方法,见以下代码:
package com.example.demo.config;
//指定为配置类
@Configuration
//指定为 Spring Security 配置类,如果是 WebFlux,则需要启用@EnableWebFluxSecurity
@EnableWebSecurity
//如果要启用方法安全设置,则开启此项。
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
//不拦截静态资源
web.ignoring().antMatchers("/static/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
//使用BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().usemameParameter("uname")
.passwordParameter("pwd")
.loginPage("/admin/login")
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
//除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
http.logout().permitAII();
http.rememberMe().rememberMeParameter("rememberme");
//处理异常,拒绝访问就重定向到403页面
http.exceptionHandling().accessDeniedPage("/403");
http.logout().logoutSuccessUrl("/");
http.csrf().ignoringAntMatchers("/admin/upload");
}
}
代码解释如下。
如果开启了CSRF, 则一定在验证页面加入以下代码以传递token值:
如果要提交表单,则需要在表单中添加以下代码以提交token值:
使用时,添加如下代码:
记住我
10.2.3配置加密方式
默认的加密方式是BCrypt;只要在安全配置类配置即可使用,见以下代码:
@Bean
public PasswordEncoder passwordEncoder() {
//使用BCrypt加密
return new BCryptPasswordEncoder();
}
在业务代码中,可以用以下方式对密码迸行加密:
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodePassword = encoder.encode(password);
10.2.4自定义加密规则
除默认的加密规则,还可以自定义加密规则。具体见以下代码:
©Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception (
auth.userDetailsService(UserService()).passwordEncoder(new PasswordEncoder(){
@Override
public String encode(CharSequence charSequence) {
return MD5Util.encode((String) charSequence);
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(MD5Util.encode((String) charSequence));
}
});
}
10.2.5配置多用户系统
一个完整的系统一般包含多种用户系统,比如“后台管理系统+前端用户系统";
Spring Security 默认只提供一个用户系统,所以,需要通过配置以实现多用户系统。
比如,如果要构建一个前台会员系统,则可以通过以下步骤来实现。
1、构建UserDetailsService用户信息服务接口
构建前端用户UserSecurityService类,并继承UserDetailsService;具体见以下代码:
public class UserSecurityService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
User user = userRepository.findByName(name);
if (user == null) {
User mobileUser = userRepository.findByMobile(name);
if (mobileUser == null) {
User emailUser = userRepository .findByEmail(name);
if (emailUser == null) {
throw new UsernameNotFoundException("用户名,邮箱或手机号不存在!");
} else {
user = userRepository.findByEmail(name);
}
} else {
user = userRepository.findByMobile(name);
}
} else if ("locked".equals(user.getStatus())) {
//被锁定,无法登录
throw new LockedException("用户被锁定”);
}
return user;
}
}
2、进行安全配置
在继承 WebSecurityConfigurerAdapter 的 Spring Security 配置类中,配置 UserSecurityService 类。
@Bean
UserDetailsService UserService() {
return new UserSecurityService();
}
多用户系统使用、配置详情,请参看本书“实战篇”。
如果要加入后台管理系统,则只需要重复上面步骤即可。
10.2.6获取当前登录用户信息的几种方式
获取当前登录用户的信息,在权限开发过程中经常会遇到。而对新人来说,不太了解怎么获取, 经常遇到获取不到或报错的问题。
所以,本节讲解如何在常用地方获取当前用户信息。
1.在Thymeleaf视图中获取
要Thymeleaf视图中获取用户信息,可以使用Spring Security的标签特性。
在Thymeleaf页面中引入Thymeleaf的Spring Security依赖,见以下代码:
未登录,单击 登录
登录名:
角色:
id:
Username: