第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值:
<head> <meta name="_csrf" th:content="${_csrf.token} " /> <!--default header name is X-CSRF-TOKEN --> <meta name="_csrf_header" th:content="${_csrf.headerName}" /> </head>
如果要提交表单,则需要在表单中添加以下代码以提交token值:
<input type="hidden" th:name="${_csrf.parameterName)" th:value="${_csrf.token)">
- http.rememberMe(): "记住我"功能,可以指定参数。
使用时,添加如下代码:
<input class="i-checks" type="checkbox" name="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依赖,见以下代码:
<!DOCTYPE html> <html lang="zh" xmlns:th=”http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <!-省略 〉 <body> <!-匿名-〉 <div sec:authorize="isAnonymous()"> 未登录,单击 <a th:href="@(/home/login)">登录</a> </div> <!-已登录- > <div sec:authorize="isAuthenticated()"> <p〉已登录</p> <p>登录名:<span sec:authentication="name"X/span></p> <p>角色:<span sec:authentication="principal.authorities"></span></p> <p>id: <span sec:authentication="principal.id"></span></p> <p>Username: <span sec:authentication="principal.username"></span></p> </div> </body> </html>
这里要特别注意版本的对应。如果引入了 thymeleaf-extras-springsecurity依赖依然获取不 到信息,那么可能是Thymeleaf版本和thymeleaf-extras-springsecurity的版本不对。
请检查 在pom.xml文件的两个依赖,见以下代码:
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-thymeleaf</artifactld>
</dependency>
<dependency>
<groupld>org.thymeleaf.extras</groupld>
<artifactld>thymeleaf-extras-springsecurity5</artifactld>
</dependency>
2,在Controller中获取
在控制器中获取用户信息有3种方式,见下面的代码注释。
@GetMapping( "userinfo")
public String getProduct(Principal principal, Authentication authentication, HttpServletRequest httpServletRequest) {
/**
* Description: 1.通过 Principal 参数获取
*/
String username = principal.getName();
/**
* Description: 2.通过 Authentication 参数获取
*/
String userName2 = authentication.getName();
/**
* Description: 3.通过 HttpServletRequest 获取
*/
Principal httpServletRequestUserPrincipal = httpServletRequest.getUserPrincipal();
String userName3 = httpServletRequestUserPrincipal.getName();
return username;
}
3.在Bean中获取
在Bean中,可以通过以下代码获取:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof AnonymousAuthenticationToken)) {
String username = authentication.getName();
return username;
}
在其他 Authentication 类也可以这样获取。比如在 UsernamePasswordAuthenticationToken 类中。
如果上面代码获取不到,并不是代码错误,则可能是因为以下原因造成的:
(1) 要使上面的获取生效,必须在继承 WebSecurityConfigurerAdapter的类中的 http.antMatcher("/*")的鉴权 URI 范围内。
(2) 没有添加 Thymeleaf 的 thymeleaf-extras-springsecurity 依赖。
(3) 添加 了 Spring Security 的依赖,但是版本不对,比如 Spring Security 和 Thymeleaf 的版本不对;
10.3 实例36:用Spring Security实现后台登录及权限认证功能
本实例通过使用Spring Security来实现后台登录及权限认证功能。
本实例的源代码可以在“/10/SpringSecuritySimpleDemo”目录下找到。
10.3.1引入依赖
使用前需要引入相关依赖,见以下代码:
<dependencies>
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-web</artifactld>
</dependency>
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-security</artifactld>
</dependency>
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-thymeleaf</artifactld>
</dependency>
<!--注释:为了能在Thymeleaf中使用Spring Security 5的特性,比如使用sec:authentication="name"显示用户名 -->
<dependency>
<groupld>org.thymeleaf.extras</groupld>
<artifactld>thymeleaf-extras-springsecurity5</artifactld>
</dependency>
</dependencies>
10.3.2创建权限开放的页面
这个页面是不需要鉴权即可访问的,以区别演示需要鉴权的页面,见以下代码:
<!DOCTYPE htmlxhtml lang="en" xmlns:th二"http://www.thymeleaf.org” xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <head><title>Spring Security 案例</title></head> <body> <h1 >Welcome!</h1 > <p><a th:href="@{/home}"> 会员中心 </a></p> </body></html>
10.3.3创建需要权限验证的页面
其实可以和不需要鉴权的页面一样,鉴权可以不在HTML页面中进行,见以下代码:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec ="http://www.thymeleaf.org/thymeleaf—extras—sp「ingsecurity5"> <headxtitle>home</title></head> <body> <h1>Hello 会员中心</h1> <p th:inline="text">Hello <span sec:authentication="name"X/span></p> <form th:action="@(/logout}" method="post"> 〈input type="submit” value="登出”/〉 </form> </body></html>
使用 Spring Security 5 之后,可以在模板中用<span sec:authentication="name"X/span> 或[[${#httpServletRequest.remoteUser}]]来获取用户名。
登岀请求将被发送到“/logout”。成功 注销后,会将用户重定向到“/login?logout”。
10.3.4 配置 Spring Security
(1)配置 Spring MVC
可以继承WebMvcConfigurer,具体使用见以下代码:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) (
//设置登录处理操作
registry.addViewController("/home").setViewName("spnngsecurity/home");
registry.addViewController("/").setViewName("springsecurity/welcome");
registry.addViewController("/login").setViewName("springsecurity/login");
}
}
(2)配置 Spring Security
Spring Security的安全配置需要继承WebSecurityConfigurerAdapter,然后重写其方法, 见以下代码:
@Configuration
@EnableWebSecurity //指定为 Spring Security 配置类
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/welcome", "/login").permitAll()
//静态资源
.antMatchers("/css/**", "/js/**").permitAll()
//需要相应的角色
.antMatchers("/admins/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
//基于form表单登录验证
.formLogin()
.loginPage("/login").defaultSuccessUrl("/home")
//启用remember me
.and().rememberMe().key(KEY)
.and().logout().permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
//指定编码方式
.withUser("admin").password("$2a$10$Q21imUyxDeshQ2tQBUfJKuBHbmuyTsZYoCMRmGi5UcOlavevauZwS")
.roles("USER");// 密码是 Izhonghua
}
}
代码解释如下。
- @EnableWebSecurity 注解:集成了 Spring Security 的 Web 安全支持。
- @WebSecurityConfig : 在配置类的同时集成了 WebSecurityConfigurerAdapter,重写 了其中的特定方法,用于自定义Spring Security配置。Spring Security的工作量都集中 在该配置类。
- configure(HttpSecurity):定义了哪些URL路径应该被拦截。
- configureGlobaI (Authentication Ma nagerBui Ider) : 在内存中配置一个用户, admin/lzhonghua,这个用户拥有User角色。
10.3.5创建登录页面
登录页面要特别注意是否开启了 CSRF功能。如果开启了,则需要提交token信息。创建的登 录页面见以下代码:
<!DOCTYPE html> chtml lang="en" xmlns:th="http://www.thymeleaf.org” xmlns:sec="http://www.thymeleaf.org/thymeleaf - ext「as-sp「ingsecurity5”> <head><title>Spring Security Example </title></head> <body> <div th:if="$(param.error}"> 无效的用户名或密码 </div> <div th:if="${param.logout}"> 你已经登出 </div> <form th:action="@(/login)" method="post"> <div><label> 用户名:〈input type="text" name=”use「name”/> </labelx/div> <div><label> 密码:<input type="password" name="password”/〉</labelx/div> 〈divXinput type ="submit" value 二”登录”/x/div> </form> </body> </html>
10.3.6测试权限
(1) 启动项目,访问首页“http://localhost:8080”,单击“会员中心”,尝试访问受限的页面 “http:〃localhost:8080/home”。
由于未登录,结果被强制跳转到登录页面uhttp://localhost: 8080/login”。
(2) 输入正确的用户名和密码(admin、Izhonghua )之后,跳转到之前想要访问的“/home:”, 显示用户名admin。
(3) 单击“登出”按钮,回到登录页面。
10.4权限控制方式
10.4.1 Spring EL权限表达式
Spring Security支持在定义URL访问或方法访问权限时使用Spring EL表达式。根据表达式 返回的值(true或false)来授权或拒绝对应的权限。
Spring Security可用表达式对象的基类是 SecurityExpressionRoot,它提供了通用的内置表达式,见表:
表达式 | 描 述 |
hasRole([role]) | 当前用户是否拥有指定角色 |
hasAnyRole([role1 ,role2]) | 多个角色以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个, 则返回true |
hasAuthority([auth]) | 等同于hasRole |
hasAnyAuthority([auth1 ,auth2]) | 等同于 hasAnyRole |
Principle | 代表当前用户的principle对象 |
authentication | 直接从Securitycontext获取的当前Authentication对象 |
permitAII | 总是返回true,表示允许所有的 |
denyAII | 总是返回false,表示拒绝所有的 |
isAnonymous() | 当前用户是否是一个匿名用户 |
isRememberMeO | 表示当前用户是否是通过Remember-Me自动登录的 |
表达式 | 描 述 |
isAuthenticated() | 表示当前用户是否已经登录认证成功了 |
isFullyAuthenticated() | 如果当前用户既不是匿名用户,又不是通过Remember-Me自动登录的,则返 回 true |
在视图模板文件中,可以通过表达式控制显示权限,如以下代码:
<p sec:authorize="hasRole('ROLE_ADMIN')" >管理员 </p> <p sec:authorize="hasRole('ROLE_USER')" >普通用户</p>
在WebSecurityConfig中添加两个内存用户用于测试,角色分别是ADMIN、USER
.withUser("admin").password("123456").roles("ADMIN")
.and().withUser("user").password("123456").roles("USER");
用户admin登录,则显示:
管理员
用户user登录,则显示:
普通用户
然后,在WebSecurityConfig中加入如下的URL权限配置:
.antMatchers("/home").hasRole("ADMIN")
这时,当用admin用户访问“home”页面时能正常访问,而用user用户访问时则会提示“403 禁止访问"。因为,
这段代码配置使这个页面访问必须具备ADMIN (管理员)角色,这就是通过 URL控制权限的方法。
10.4.2通过表达式控制URL权限
如果要限定某类用户访问某个URL,则可以通过Spring Security提供的基于URL的权限控 制来实现。
Spring Security 提供的保护 URL 的方法是重写 configure(HttpSecurity http)方法, HttpSecurity提供的方法见表:
方法名 | 用 途 |
access(String) | SpringEL表达式结果为true时可访问 |
anonymous() | 匿名可访问 |
denyAII() | 用户不可以访问 |
fullyAuthenticated() | 用户完全认证访问(非“remember me”下的自动登录) |
hasAnyAuthority(String …) | 参数中任意权限可访问 |
hasAnyRole(String …) | 参数中任意角色可访问 |
hasAuthority(String) | 某一权限的用户可访问 |
hasRole(String) | 某一角色的用户可访问 |
permitAHO | 所有用户可访问 |
rememberMe() | 允许通过“remember me”登录的用户访问 |
authenticated() | 用户登录后可访问 |
haslpAddress(String) | 用户来自参数中的IP可访问 |
还需要额外补充以下几点。
- authenticated():保护 URL,需要用户登录。如:anyRequest().authenticated。代表其 他未配置的页面都已经授权。
- permitAHO:指定某些URL不进行保护。一般针对静态资源文件和注册等未授权情况下需 要访问的页面。
- hasRole(String role):限制单个角色访问。在Spring Security中,角色是被默认增加 “ROLE_"前缀的,所以角色"ADMIN” 代表 UROLE_ADMIN"。
- hasAnyRole(String- roles):允许多个角色访问。这和Spring Boot! x版本有所不同。 ・access(String attribute):该方法可以创建复杂的限制,比如可以増加RBAC的权限表达式。
- haslpAddress(String ipaddressExpression):用于限制 IP 地址或子网。
具体用法见以下代码:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/static", "/register").permitAll()
.antMatchers("/user/**").hasAnyRole("USER","ADMIN")
//代表"/admin/”下的所有URL只允许IP为100.100.100.100"且用户角色是“ADMIN”的用户访问
.antMatchers("/admin/**").access("hasRole('ADMIN') and haslpAddress('100.100.100.100)")
//其他未配置的页面都已经授权
.anyRequest().authenticated()
}
10.4.3通过表达式控制方法权限
要想在方法上使用权限控制,贝IJ需要使用启用方法安全设置的注解@EnableGlobalMethod- SecurityOo它默认是禁用的,需要在继承WebSecurityConfigurerAdapter的类上加注解来启用,
还需要配置启用的类型,它支持开启如下三种类型。
- @EnableGlobalMethodSecurity(jsr250Enabled= true):开启 JSR-250。
- @EnableGlobalMethodSecurity(prePostEnabled = true):开启 prePostEnabledo
- @EnableGlobalMethodSecurity(securedEnabled= true):开启 secured。
1、JSR-250
JSR是Java Specification Requests的缩写,是Java规范提案。任何人都可以提交JSR, 以向Java平台增添新的API和服务。JSR是Java的一个重要标准。
Java 提供了很多 JSR,比如 JSR-250. JSR-303、JSR-305、JSR-308o 初学者可能会 对JSR有疑惑。大家只需要记住“不同的JSR其功能定义是不一样的”即可。比如,JSR-303 主要是为数据的验证提供了一些规范的APIo这里的JSR-250是用于提供方法安全设置的,它主 要提供了注解@RolesAllowed0
它提供的方法主要有如下几种:
- @DenyAII:拒绝所有访问。
- @RolesAllowed({”USER”,“ADMIN”}):该方法只要具有”USER”、"ADMIN'任意一种权限就可以访问。
- @PermitAII:允许所有访问。
2、prePostEnabled
除JSR-250注解外,还有prePostEnabled注解,它也是基于表达式的注解,并可以通过继 承GlobalMethodSecurityConfiguration类来实现自定义功能。
如果没有访问方法的权限,则会抛 出 AccessDeniedException
它主要提供以下4种功能注解:
(1 ) @PreAuthorize
它在方法执行之前执行,使用方法如下:
①限制userld的值是否等于principal中保存的当前用户的userid,或当前用户是否具有 ROLE_ADMIN 权限。
@PreAuthorize("#userld == authentication.principal.userid or hasAuthority('ADMIN')")
② 限制拥有ADMIN角色才能执行。
@PreAuthorize("hasRole('ROLE_ADMlN')")
③ 限制拥有ADMIN角色或USER角色才能执行。
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
④ 限制只能查询id小于3的用户才能执行。
@PreAuthorize("#id<3")
⑤ 限制只能查询自己的信息,这里一定要在当前页面经过权限验证,否则会报错。
@PreAuthorize("principal.username.equals(#usemame)")
⑥ 限制用户名只能为long的用户。
@PreAuthorize("#user.name.equals('long')")
对于低版本的Spring Security,添加注解之后还需要将AuthenticationManager定义为Bean,具体见以下代码:
@Bean
©Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
AuthenticationManager authenticationManager;
(2) @PostAuthorize
表示在方法执行之后执行,有时需要在方法调用完后才进行权限检查。可以通过注解 @PostAuthorize 达到这一效果。
注解@PostAuthorize是在方法调用完成后进行权限检查的,它不能控制方法是否能被调用, 只能在方法调用完成后检查权限,来决定是否要抛出AccessDeniedException
这里也可以调用方法的返回值。如果EL为false,那么该方法已经执行完了,可能会回滚。EL 变量retumObject表示返回的对象,如:
@PostAuthorize("retumObject.userld == authentication.principal.userid or hasPermission(returnObject, 'ADMIN*)");
(3) @PreFilter
表示在方法执行之前执行。它可以调用方法的参数,然后对参数值进行过滤、处理或修改。EL 变量filterobject表示参数。如有多个参数,贝U使用filterTarget注解参数。方法参数必须是集合或 数组。
(4) @postFilter
表示在方法执行之后执行。而且可以调用方法的返回值,然后对返回值进行过滤、处理或修改, 并返回。EL变量retumObject表示返回的对象。方法需要返回集合或数组。
如使用@PreFilter和@PostFilter时,Spring Security将移除使对应表达式的结果为false 的元素。
当Filter标注的方法拥有多个集合类型的参数时,需要通过filterTarget属性指定当前是针对哪 个参数进行过滤的
3. securedEnabled
开启securedEnabled支持后,可以使用注解©Secured来认证用户是否有权限访问。使用 方法见以下代码:
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("ROLE_TELLER")
10.4.4实例37:使用JSR-250注解
本实例演示如何使用JSR-250注解。如果读者阅读本节代码有一定困难,建议直接使用下面 提供的源代码进行演练。
本实例的源代码可以在*710/JSR_250Demo”目录下找到。
(1 )开启支持。
在安全配置类中,启用注解@EnableGlobalMethodSecurityGsr250Enabled=true)
(2) 创建user服务接口 UserService,见以下代码:
public interface UserService {
public String addUser();
public String updateUser() ;
public String deleteUser();
}
(2) 实现user服务接口的方法,见以下代码:
@Service
public class UserServicelmpI implements UserService {
@Override
public String addUser() {
System.out.println("addUser");
return null;
}
@Override
@RolesAllowed({"ROLE_USER","ROLE_ADMIN"})
public String updateUser() {
System.out.println("updateUser");
return null;
}
@Override
@RolesAllowed("ROLE_ADMIN")
public String deleteUser() {
System.out.println("delete");
return null;
}
}
(2) 编写控制器,见以下代码:
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/addUser")
public void addUser() {
userService.addUser();
}
@GetMapping("/updateUser")
public void updateUser() {
userService.updateUser();
}
@GetMapping("/delete")
public void delete() {
userService.deleteUser();
}
}
(2) 测试。
启动项目,访问uhttp://localhost:8080/user/addUser" , 则控制台输出提示:
addUser
访问 http://localhost:8080/user/delete 和 http://localhost:8080/user/updateUser,,, 则会提示没有权限:
There was an unexpected error (type二Forbidden, status=403).
Access Denied
10.4.5实例38:实现RBAC权限模型
本实例介绍在Spring Security配置类上配置自定义授权策略,可以通过加入access属性和 URL判断来实现RBAC权限模型的核心功能。
昌本实例的源代码可以在"/10/RbacDemo”目录下.找到
RBAC模型简化了用户和权限的关系。通过角色对用户进行分组,分组后可以很方便地进行权 限分配与管理。RBAC模型易扩展和维护。下面介绍具体步骤。
(1 )创建RBAC验证服务接口。
用于权限检查,见以下代码:
public interface RbacService {
boolean check(HttpServletRequest request, Authentication authentication);
}
(2 )编写RBAC服务实现,判断URL是否在权限表中。
要实现RBAC服务,步骤如下:
① 通过注入用户和该用户所拥有的权限(权限在登录成功时已经缓存起来,当需要访问该用户 的权限时,直接从缓存取岀)验证该请求是否有权限,有就返回true,没有则返回false,不允许访 问该URLo
② 传入request,可以使用request获取该次请求的类型。
③ 根据Restful风格使用它来控制的权限。如请求是POST,则证明该请求是向服务器发送一 个新建资源请求,可以使用request.getMethod()来获取该请求的方式。
④ 配合角色所允许的权限路径进行判断和授权操作。
⑤ 如果获取到的Principal对象不为空,则代表授权已经通过。
本实例不针对HTTP请求进行判断,只根据URL进行鉴权,具体代码如下:
@Component("rbacService")
public class RbacServicelmpI implements RbacService {
private AntPathMatcher AntPathMatcher = new AntPathMatcher();
@Autowired
private SysPermissionRepository permissionRepository;
@Autowired
private SysUserRepository sysUserRepository;
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
boolean hasPermission = false;
if (principal != null && principal instanceof UserDetails) (
//登录的用户名
String userName = ((UserDetails) principal).getllsemame();
//获取请求登录的URL
Set<String> urls = new HashSet<>();//用户具备的系统资源集合,从数据库读取
SysUser sysUser = sysUserRepository.findByName(userName);
try {
for (SysRole role : sysUser.getRoles()) (
for (SysPermission permission : role.getPermissions()) {
urls.add(permission.getUrl());
}
}
) catch (Exception e) (
e.printStackTrace();
}
for (String url: urls) {
if (AntPathMatcher.match(url, request.getRequestURI())) (
hasPermission = true;
break;
}
}
}
return hasPermission;
}
}
(3) 配置 HttpSecurity
在继承 WebSecurityConfigurerAdapter 的类中重写 void configure(HttpSecurity http)方 法,添加如下代码:
.antMatchers("/admin/**").access("@rbacService.check(request,authentication)")
这里注意,@rbacService接口的名字是服务实现上定义的名字,即注解@Component("rbacService") 定义的参数。具体代码如下:
@EnableGlobalAuthentication
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.authorizeRequests()
.antMatchers("admin").permitAll()
//使用自定义授权策略
.antMatchers("admin/rbac").access("@rbacService.hasPermission(request, authentication)");
}
}
(4) 创建实体,添加测试数据。
这里要创建3个实体,分别是用户、权限和角色实体。读者请根据本书第8章的知识来创建。 创建完成后需要添加数据,可以在MySQL中执行以下代码,添加用户数据:
INSERT INTO 'sys_user' ('id', 'cnname', 'enabled', 'name', 'password') VALUES (1, NULL,", 'admin', ,$2a$10$K3BsMi6yjqk0q7AbWLJ.yeKWiJ9xSMqGN/x6WYPR/c805KBx45RL6,);
INSERT INTO 'sys_permission' ('id', 'available', 'name', 'parentjd', 'parentjds', 'permission', 'resource_type', 'url') VALUES (1, NULL, NULL, NULL, NULL, 'rbac', 'menu', 7admin/rbac');
INSERT INTO 'sys_role' ('id', 'available', 'cnname', 'description', 'role') VALUES (1, T, 'admin', NULL, 'ROLE_ADMIN');
INSERT INTO 'sys_role_permission' ('rolejd', 'permissionjd') VALUES (1,1);
INSERT INTO 'sys_user_role' ('role_id', 'uid') VALUES (1,1);
(5) 启动项目后进行测试。
① 访问 http://localhost:8080/admin/rbac,会提示无权访问,跳转至登录页面,http://localhost: 8080/admin/login
② 在登录页面输入用户名、密码(admin/lzh )登录,会提示登录成功。
③ 访问 “http:〃localhost:8080/admin/rbac”,提示访问成功。
10.5 认识 JWT
JWT ( JSON Web Token )是一个开放的标准,用于在各方之间以JSON对象安全地传输信 息。这些信息通过数字签名进行验证和授权。可以使用■■RSA"的"公钥/私钥对”对JWT进行签名。
1. JWT请求流程
(1 )用户使用浏览器(客户端)发送账号和密码。
(2)服务器使用私钥创建一个JWT。
(3)服务器返回该JWT给浏览器。
(4 )浏览器将该JWT串在请求头中向服务器发送请求。
(5) 服务器验证该JWT0
(6) 根据授权规则返回资源给浏览器。
通过前3个步骤获取了 JWT之后,在JWT有效期内,以后都不需要进行前3个步骤的操作, 直接进行第(4)-(6)步的请求资源即可。
2. JWT组成
JWT 的格式为:Header.Payload.Signature,即 JWT 包含 3 部分,为 header、payload 和 signature
(1 )头部(header )。
header是通过Base64编码生成的字符串,header中存放的内容说明编码对象是一个JWT, 并使用"SHA-256”的算法进行加密(加密用于生成Signature)。
(2)载荷(payload )。
payload主要包含claim, claim是一些实体(通常指用户)的状态和额外的元数据,有三种类 型:Reserved. Public 和 Private。
① Reserved claim是JWT预先定义的claim,在JWT中推荐使用。常用的元素有如下 几种。
- iss: Issuer,用于说明该JWT是由谁签发的。
- sub: Subject,用于说明该JWT面向的对象或面向的用户。
- aud: Audience:用于说明该JWT发送的用户是接收方。
- exp: Expiration Time,数字类型,说明该JWT过期的时间。
- nbf: Not Before,数字类型,说明在该时间之前JWT不能被接收与处理。
- iat: Issued At:数字类型,说明该JWT何时被签发。
- jti: JWT ID,标明 JWT 的唯一ID。
- user-definde1:自定义属性。
② Public claim根据需要定义自己的字段。
③ Private claim自定义的字段,可以用来在双方之间交换信息负载。如:{"sub": "12345678", "name": "long", "admin": true),
它需要经过 Base64Url 编码后作为 JWT 结构的第 二部分。
(3) signature
签名需要使用编码后的header和payload及一个密钥,使用header中指定签名算法进行签 名。流程如下:
① 将header和claim分别使用Base64进行编码,生成字符串header和payload o
② 将header和payload以header.payload的格式组合在一起,形成一个字符串。
③ 使用上面定义好的加密算法和一个存放在服务器上用于进行验证的密匙来对这个字符串进 行加密,形成一个新的字符串,这个字符串就是signatureo
10.6实例39:用JWT技术为Spring Boot的API增加认证和授权保护
在生产环境中,对发布的API增加授权保护是非常必要的。JWT作为一个无状态的授权校验技 术,非常适合于分布式系统架构。
服务器端不需要保存用户状态,因此,无须采用Redis等技术来 实现各个服务节点之间共享Session数据。
本节通过实例讲解如何用JWT技术进行授权认证和保护。
本实例的源代码可以在"/10/JwtDemo"目录下找到。
10.6.1配置安全类
JWT的安全配置也需要继承WebSecurityConfigurerAdapter,然后重写其方法。具体见以 下代码:
public class WebSecurityConfigJwt extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler jwtAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler jwtAuthenticationFailHander;
//装载BCrypt密码编码器
@Bean
public PasswordEncoder passwordEncoder3() {
//使用BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/jwt/**").
//指定登录认证的Controller
formLogin().usernameParameter("name").passwordParameter("pwd").loginPage("/jwt/login")
.successHandlerCwtAuthenticationSuccessHandler)
.failureHandler(wtAuthenticationFailHander)
.and()
.authonzeRequests()
//登录相关
.antMatchers("/register/mobile").permitAll()
.antMatchers("/article/**").authenticated()
.antMatchers("/jwt/tasks/**").hasRole("USER")
//.antMatchers(HttpMethod.POST, "/jwt/tasks/**").hasRole("USER")
.and()
//.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()));
http.logout().permitAll();
//http.rememberMe().rememberMeParameter("rememberme");//记住这个功能
//JWT配置
http.antMatcher("/article/**").addFilter(new JWTAuthenticationFilter(authenticationManager()));
http.cors().and().csrf().ignoringAntMatchers("/jwt/**");
}
@Bean
UserDetailsService JwtUserSecurityService() {
return new JwtUserSecurityService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(JwtUserSecurityService())
.passwordEncoder(
new BCryptPasswordEncoder() {
});
}
}
从上面代码可以看出,此处JWT的安全配置和上面已经讲解过的安全配置并无区别,没有特别 的参数需要配置。
10.6.2处理注册
编写注册控制器,在真实的生产环境中,笔者建议将逻辑写在Service的实现层,这里是通过 用户名、手机号和密码进行注册的。
@RestController
@RequestMapping("jwt")
public class JwtUserController extends BaseController {
@Autowired
private UserRepository userRepository;
@Autowired
private UserRoleRepository userRoleRepository;
@RequestMapping(value = "/register/mobile", method = RequestMethod.POST)
public Response regist(User user) {
try(
User userName = userRepository.findByName(user.getName());
if (null != userName) (
return result(ExceptionMsg.UserNameUsed);
}
User userMobile = userRepository.findByMobile(user.getMobile());
if (null != userMobile) (
return result(ExceptionMsg.MobileUsed);
}
//String encodePassword = MD5Util.encode(password);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
user.setPassword(encoder.encode(user.getPassword()));
user.setCreateTime(DateUtils.getCurrentTime());
user.setLastModifyTime(DateUtils.getCurrentTime());
user.setProfilePicture("img/avater.png");
List<UserRole> roles = new ArrayList<>();
UserRole rolel = userRoleRepository.findByRolename("ROLE_USER");
roles.add(rolel);
user.setRoles(roles);
userRepository.save(user);
} catch (Exception e) (
return result(ExceptionMsg.FAILED);
}
return result();
}
}
10.6.3处理登录
1.创建用于多方式登录的安全验证的服务类
这里通过多方式进行登录验证,具体见以下代码:
package com.example.demo.service.jwt;
//@Service
public class JwtUserSecurityService 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("ffi户名、邮箱或手机号不存在!");
} else{
user = userRepository.findByEmail(name);
}
} else {
user = userRepository.findByMobile(name);
}
}
return user;
}
}
2.编写登录成功处理类
登录验证成功后,需要进行成功验证的后续处理,见以下代码:
@Component("jwtAuthenticationSuccessHandler")
public class JwtAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
//用户名和密码正确执行
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws lOException, ServletException {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof UserDetails) (
UserDetails user = (UserDetails) principal;
httpServletRequest.getSession().setAttribute("userDetail", user);
String role = "";
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
String token = JwtTokenUtils.createToken(user.getUsemame(), role, true);
//返回创建成功的token
//但是,这里创建的token只是单纯的token
//按照JWT的规定,最后请求时应该是'Bearertoken'
httpServletResponse.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"ok\", \"message\":\"登录成功\"}");
out.flush();
out.close();
}
}
}
3,编写登录失败处理类
登录验证失败后,需要进行后续处理,见以下代码:
@Component("jwtAuthenticationFailHander")
public class JwtAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler {
//用户名密码错误执行
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws lOException, ServletException, lOException {
httpServletRequest.setCharacterEncoding("UTF-8");
//获得用户名
String username = httpServletRequest.getParameter("uname");
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"error\",\"message\":\"用户名或密码错误\"}");
out.flush();
out.close();
}
}
10.6.4测试多方式注册和登录
1.测试注册功能
这里使用测试工具Postman提交POST注册请求,如图10-1所示。
正确提交3个参数,显示注册成功,再次单击发送POST数据,则出现如下提示:
{
"rspCode": "000102",
"rspMsg": "该登录名称已存在"
}
说明注册功能实现成功。
2.测试多种方式登录功能
这里可以通过“手机号+密码"或“用户名+密码”的方式进行登录,登录地址是在安全配置类 配置好的地址。先用用户名和密码登录,如图10-2所示,登录成功。然后用"手机号+密码”的方 式登录,也提示成功,如图
1.创建token测试控制器
token测试控制器用于登录token后,验证权限状态,见以下代码:
@RestController
@RequestMapping("/jwt/tasks")
public class TaskController {
@GetMapping
public String listTasks(){
return "任务列表";
}
@PostMapping
public String newTasks(){
return "角色ROLE,创建了一个新的任务";
}
}
2. token 登录
通过上面方式登录成功之后,会返回token,如图10-4所示。
(1 )把获得的token值填入图10-4中的Authorization的输入框位置,在"Content-Type"处填写 "application/x-www-form-urlencoded",
用 POST 方式访问 "http://localhost:8080/jwt/tasks”,进行权限测试,会显示如下信息,代表token方式认证和授权成功。
角色ROLE,创建了一个新的任务
(2)把token值随意修改一下,然后用POST方式访问,则输岀如下提示:
{
"timestamp": "2019-04-11T13:49:52.817+0000",
"status": 500,
"error": "Internal Server Error",
"message": "JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
"path": "/jwt/tasks"
}
从上面代码可以看出,签名错误,认证失败。
10.7 ShiroApache通用安全框架
10.7.1认识Shiro安全框架
除Spring Security安全框架外,应用非常广泛的就是Apache的强大又灵活的开源安全框架 Shiro,在国内使用量远远超过Spring Security。它能够用于身份验证、授权、加密和会话管理, 有易于理解的API,可以快速、轻松地构建任何应用程序。
而且大部分人觉得从Shiro入门要比 Spring Security 简单。
10.7.2认识Shiro的核心组件
Shiro有如下核心组件。
- Subject:代表当前“用户”。与当前应用程序交互的任何东西都是Subject,如爬虫、机 器人。所有Subject都绑定到SecurityManager,与Subject的所有交互者B会委托给 SecurityManager。Subject 是一个门面,SecurityManager 是实际的执行者。
- SecurityManager:与安全有关的操作都会与SecurityManager交互。它管理着所有 Subject,是Shiro的核心,负责与其他组件进行交互。
- Realm: Shiro从Realm中获取安全数据(用户、角色、权限)。SecurityManager需要 从Realm中获取相应的用户信息进行比较用户身份是否合法,也需要从Realm中得到用户 相应的角色/权限进行验证,以确定用户是否能进行操作。
10.8 实例40:用Shiro实现管理后台的动态权限功能
本实例使用Shiro来实现管理后台的动态权限功能。
本实例的源代码可以在"/W/ShiroJpaMysql”目录下找到。
10.8.1创建实体
1、创建管理员实体
创建管理实体,用于存放管理员信息,见以下代码:
@Entity
public class Admin implements Serializable (
@ld
@GeneratedValue
private Integer uid;
@Column(unique =true)
/**
* 账号
*/
private String username;
/**
* 名称
*/
private String name;
/**
* 密码
*/
private String password;
/**
* 加密密码的盐
*/
private String salt;
/**
* 用户状态:0,创建未认证(比如没有激活、没有输入验证码等),等待验证的用户;1,正常状态;2,用户被 锁定.
*/
private byte state;
/**
* 立即从数据库中加载数据
*/
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid")), inverseJoinColumns ={@JoinColumn(name = "roleld")))
/**
* 一个用户具有多个角色
*/
private List<SysRole> roleList;
}
- 创建权限实体
权限实体用于存放权限数据,见以下代码:
@Entity
@Data
public class SysPermission implements Serializable {
/**
* 主键
*/
@Id
@GeneratedValue
private Integer id;
/**
* 权限名称
*/
private String name;
@Column(columnDefinition="enum('menu','button')")
* 资源类型,[menulbutton]
*/
private String resourceType;
/**
* 资源路径
*/
private String url;
/**
* 权限字符串
*/
private String permission;
//menu 例子:role:*,
//button 例子:role:create,role:update,role:delete,role:view
/**
* 父编号
*/
private Long parentld;
/**
* 父编号列表
*/
private String parentlds;
private Boolean available = Boolean.FALSE;
@ManyToMany
@JoinTable(name="SysRolePermission",joinColumns=(@JoinColumn(name="permissionld")},inversejoin Columns=(@JoinColumn(name="roleld")))
private List<SysRole> roles;
}
3.创建角色实体
角色实体是管理员的角色,用于对管理员分组,并通过与权限表映射来确定管理员的权限,见 以下代码:
@Entity
@Data
public class SysRole {
/**
* 编号
*/
@Id
@GeneratedValue
private Integer Id;
@Column(unique =true)
/**
* 角色标识程序中判断使用,如”admin”,这个是唯一的
*/
private String role;
/**
* 角色描述,Ul界面显示使用
*/
private String description;
/**
* 是否可用,如果不可用,则不会添加给用户
*/
private Boolean available = Boolean.FALSE;
/**
* 角色权限关系:多对多关系
*/
@ManyToMany(fetch= FetchType.EAGER)
@JoinTable(name-,SysRolePermission",joinColumns={@JoinColumn(name=,,roleldn)},inverseJoinColumn s=(@JoinColumn(name="permissionld")))
private List<SysPermission> permissions;
/**
* 用户角色关系定义
*/
@ManyToMany
@JoinTable(name="SysUserRole",joinColumns=(@JoinColumn(name=,Toleld")),inverseJoinColumns={@ JoinColumn(name="uid")))
/**
* 一个角色对应多个用户
*/
private List<Admin> admins;
}
10.8.2实现视图模板
(1) 完成登录页面编码,见以下代码:
<!—省略...一> <body> 错误信息:<p th:text="${msg}"></p> <form action="" method="post"> <p>账号:<input type="text" name="usemame" value="long7X/p> <p>密码:<input type="text" name="password" value="123456"/></p> <p><input type="submit" value="登录 7><7p> </form> </body></html>
(2)实现会员中心页面,见以下代码:
<!--省略...一> <body> <h1>home</h1> <a href="http://localhost:8080/admin/add">添加v/a〉 <a href="http://localhost:8080/admin/del">删除</a> <a href="http://localhost:8080/admin/list">用户列表<7a> <a href="http://localhost:8080/admin/login">登录</a> <a href="http://localhost:8080/logout">登出</a> </body> </html>
10.8.3进行权限配置
权限配置的步骤为:先拿到用户信息,然后根据用户信息查询到角色,再通过角色查询到权限, 最后存入SimpleAuthorizationlnfo。见以下代码:
/**
* 权限配置
*/
@Override
protected Authorizationinfo doGetAuthorizationlnfo(PrincipalCollection principals) {
//拿到用户信息
SimpleAuthorizationlnfo info = new SimpleAuthorizationlnfo();
Admin admininfo = (Admin) principals.getPrimaryPrincipal();
for (SysRole role : admininfo.getRoleList()) {
//将角色放入 SimpleAuthorizationlnfo
info.addRole(role.getRole());
//用户拥有的权限
for (SysPermission p : role.getPermissions()) (
info.addStringPermission(p.getPermission());
}
}
return info;
}
10.8.4实现认证身份功能
进行身份认证,判断用户名、密码是否匹配正确,见以下代码:
@Override
protected Authenticationinfo doGetAuthenticationlnfo(AuthenticationToken token) throws AuthenticationException {
//获取用户输入的账号
String username = (String) token.getPrincipal();
System.out.println(token.getCredentials());
//通过username从数据库中查找Use「对象。
//Shiro有时间间隔机制,两分钟内不会重复执行该方法
//获取用户信息
Admin admininfo = adminDao.findByUsername(username);
if (admininfo == null) {
return null;
}
SimpleAuthenticationlnfo info =
new SimpleAuthenticationlnfo(admininfo,
admininfo.getPassword(),
ByteSource.Util.bytes(adminlnfo.getCredentialsSalt()),
getName());
return info;
}
10.8.5测试权限
(1 )请用下面的SQL代码插入测试数据到数据库。
INSERT INTO 'admin' VALUES (1,'管理员','32baebda76498588dabf64c6e8984097', 'yan', 'O', 'long');
INSERT INTO 'sys_permission'
('id','available','name','parent_id','parent_ids','permission','resource_type','url')VALUES (1,0,'用户管理',0,'0/','admin:view','menu','admin/list');
INSERT INTO 'sys_permission'
('id','available','name','parent_id','parent_ids','permission','resource_type','url') VALUES (2,0,'用户添加',1,'0/','admin:add','button','admin/add');
INSERT INTO 'sys_permission'
('id','available','name','parent_id','parent_ids','permission','resource_type','url') VALUES (3,0,'用户删除',1,'0/1','admin:del','button','admin/del');
INSERT INTO 'sys_role'('id','available','description','role') VALUES(1,'0', '管理员','admin');
INSERT INTO 'sys_role_permission' ('permission_id','role_id') VALUES (1,1);
INSERT INTO 'sys_role_permission' ('permission_id','role_id') VALUES (2,1);
INSERT INTO 'sys_role_permission' ('permission_id','role_id') VALUES (3,1);
INSERT INTO 'sys_user_role' ('role_id', 'uid') VALUES (1,1);
(2)测试用户添加。
使用用户名、密码(long/longzhonghua )登录,进行测试。
访问“http:〃localhost:8080/admin/adcT,网页会提示如下信息:
403没有权限
(3 )测试用户管理。
访问uhttp://localhost:8080/admin/list",网页会显示用户列表,代表有权限,这是默认的权 限。若有其他的测试需求,则可以自行添加用户、角色和权限进行测试。
10.9 对比 Spring Security 与 Shiro
可以看到,Shiro的关注度要远远高于Spring Security0
(1) Shiro的特点。
- 功能强大,且简单、灵活。
- 拥有易于理解的API。
- 简单的身份认证(登录),支持多种数据源(LDAP、JDBCs Kerberos、ActiveDirectory等)。
- 支持对角色的简单签权,并且支持细粒度的签权。
- 支持一级缓存,以提升应用程序的性能。
- 内置的基于POJO会话管理,适用于Web,以及非Web环境。
- 不跟任何的框架或容器捆绑,可以独立运行。
(2) Spring Security 的特点。
- Shiro的功能它都有。
- 对防止CSRF跨站、XSS跨站脚本可以很好地实现,对Oauth、OpenlD也有支持。Shiro 则需要幵发者自己手动实现。
- 因为Spring Security是Spring自己的产品,所以对Spring的支持极好,但也正是因为这 个,所以仅仅支持自己的产品,导致其捆绑到了 Spring框架,而不支持其他框架。
- Spring Security的权限细粒度更高(这不是绝对的,Shiro也可以实现)。