《Spring Boot 实战派》--10.集成安全框架,实现安全 认证和授权
第10章 集成安全框架,实现安全 认证和授权
本章首先介绍如何使用Spring Security创建独立验证的管理员权限系统、会员系统,讲解如 何进行分表、分权限' 分登录入口、分认证接口、多注册接口,以及RBAC权限的设计和实现,如何使用JWT为手机APP提供token认证;
然后讲解Apache的Shiro安全框架的基本理论基础, 以及如何使用Shiro构建完整的用户权限系统;
最后对比分析Spring Security和Shiro的区别。
10.1 Spring Security Spring 的安全框架
10.1.1 认识 Spring Security
Spring Security提供了声明式的安全访问控制解决方案(仅支持基于Spring的应用程序),对 访问权限进行认证和授权,它基于Spring AOP和Servlet过滤器,提供了安全性方面的全面解决 方案。
除常规的认证和授权外,它还提供了 ACLs、LDAP、JAAS、CAS等高级特性以满足复杂环 境下的安全需求。
1.核心概念
Spring Security的3个核心概念。
- Principle:代表用户的对象Principle ( User),不仅指人类,还包括一切可以用于验证的 设备。
- Authority:代表用户的角色Authority ( Role ),每个用户都应该有一种角色,如管理员或 是会员。
- Permission:代表授权,复杂的应用环境需要对角色的权限进行表述。
在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.模块
- 核心模块 spring-security-core.jar:包含核心验证和访问控制类和接口,以及支持远程配置的基本APL
- 远程调用 spring-security-remoting.jar:提供与 Spring Remoting 集成。
- 网页 spring-security-web.jar:包括网站安全的模块,提供网站认证服务和基于URL访问控制。
- 配置 spring-security-config.jar:包含安全命令空间解析代码。
- LDAP spring-security-ldap.jar: LDAP 验证和配置。
- ACL spring-security-acl.jar:对 ACL 访问控制表的实现。
- CAS spring-security-cas.jar;对 CAS 客户端的安全实现。
- OpenlD spring-security-openid.jar:对 Open ID 网页验证的支持。
- Test spring-security-test.jar:对 Spring Security 的测试的支持。
10.1.2核心类
1、Securitycontext
Securitycontext中包含当前正在访问系统的用户的详细信息,它只有以下两种方法。
- getAuthentication():获取当前经过身份验证的主体或身份验证的请求令牌。
- setAuthentication():更改或删除当前已验证的主体身份验证信息。
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来实现。
- GlobalSecurityContextHolderStrategy:表示全局使用同一个 SecurityContext,如 C/S 结构的客户端。
- InheritableThreadLocalSecurityContextHolderStrategyJJffl InheritableThreadLocal 来存放Securitycontext,即子线程可以使用父线程中存放的变量。
- ThreadLocalSecurityContextHolderStrategy: 使用ThreadLocal 来存放 SecurityContext;
—般情况下,使用默认的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, 则有下面两种方法:
- 通过 SecurityContextHolder的静态方法 setStrategyName(java.lang.String strategyName) 来改变需要使用的strategy ;
- 通过系统属性(SYSTEM_PROPERTY) 行指定,其中属性名默认为"spring.security. strategy",属性值为对应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中提供了以下几种方法。
- String getPassword():返回验证用户密码,无法返回则显示为null。
- String getUsemame():返回验证用户名,无法返回则显示为null。
- boolean isAccountNonExpired():账户是否过期,过期无法验证。
- boolean isAccountNonLocked():指定用户是否被锁定或解锁,锁定的用户无法逬行身份 验证。
- boolean isCredentialsNonExpired():指定是否已过期的用户的凭据(密码),过期的凭 据无法认证。
- boolean isEnabledQ:是否被禁用。禁用的用户不能进行身份验证。
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 配置 Spring Security
10.2.1 继承 WebSecurityConfigurerAdapter
通过重写抽象接口 WebSecurityConfigurerAdapter,再加上注解@EnableWebSecurity, 可以实现Web的安全配置。
WebSecurityConfigurerAdapter Config 模块一共有 3 个 builder (构造程序)。
- AuthenticationManagerBuilder:认证相关builder,用来配置全局的认证相关的信息。它包含Authenticationprovider和UserDetailsService,前者是认证服务提供者,后者是用 户详情查询服务。
- HttpSecurity:进行权限控制规则相关配置。
- WebSecurity:进行全局请求忽略规则配置、HttpFirewall配置、debug配置、全局 SecurityFilterChain 配置。
配置安全,通常要重写以下方法:
//通过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"); } }
代码解释如下。
- authorizeRequests():定义哪些URL需要被保护,哪些不需要被保护。
- antMatchers("/admin/** ").hasRole("ADMIN"):定义/admin/下的所有 URL。只有拥有 admin角色的用户才有访问权限。
- formLogin() :自定义用户登录验证的页面。
- http.csrf() :配置是否开启CSRF保护,还可以在开启之后指定忽略的接口。
如果开启了CSRF, 则一定在验证页面加入以下代码以传递token值:
如果要提交表单,则需要在表单中添加以下代码以提交token值:
- http.rememberMe(): "记住我"功能,可以指定参数。
使用时,添加如下代码:
记住我
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: