数据库能上传网站模板,做网站用cms好吗,博物馆建设网站,在线网站代码生成器Spring Security 应用详解 集成SpringBootSpring Boot 介绍创建maven工程spring 容器配置Servlet Context配置安全配置测试 工作原理结构总览认证流程认证流程AuthenticationProviderUserDetailsServicePasswordEncoder 授权流程授权流程授权决策 自定义认证自定义登录页面认证… Spring Security 应用详解 集成SpringBootSpring Boot 介绍创建maven工程spring 容器配置Servlet Context配置安全配置测试 工作原理结构总览认证流程认证流程AuthenticationProviderUserDetailsServicePasswordEncoder 授权流程授权流程授权决策 自定义认证自定义登录页面认证页面配置认证页面安全配置测试 连接数据库认证创建数据库代码实现定义UserDetailService测试使用BCryptPasswordEncoder 会话获取用户身份会话控制会话超时安全会话cookie 退出授权概述准备环境数据库环境修改UserDetailService web授权方法授权 集成SpringBoot
Spring Boot 介绍
Spring Boot是一套Spring的快速开发框架基于Spring4.0设计使用Spring Boot开发可以避免一些繁琐的工程搭建和配置同时它集成了大量的常用框架快速导入依赖包避免依赖包的冲突。基本上常用的开发框架都支持Spring Boot开发例如MyBatis、Dubbo等Spring家族更是如此例如Spring cloud、Spring mvc、Spring Security等使用Spring Boot开发可以大大提高生产效率所以Spring Boot的使用率非常高。
本章节讲解如何通过Spring Boot开发Spring Security应用Spring Boot提供spring-boot-starter-security用于开发Spring Security应用。
创建maven工程
1)创建maven工程security-spring-boot工程结构如下
2)引入以下依赖
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.1.3.RELEASE/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.mry.security/groupIdartifactIdsecurity-spring-boot/artifactIdversion0.0.1-SNAPSHOT/versionnamesecurity-spring-boot/namedescriptionDemo project for Spring Boot/descriptionpropertiesproject.build.sourceEncodingUTF-8/project.build.sourceEncodingmaven.compiler.source1.8/maven.compiler.sourcemaven.compiler.target1.8/maven.compiler.target/propertiesdependencies!-- 以下是spring boot依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- 以下是spring security依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependency!-- 以下是jsp依赖--dependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdscopeprovided/scope/dependency!--jsp页面使用jstl标签 --dependencygroupIdjavax.servlet/groupIdartifactIdjstl/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-tomcat/artifactIdscopeprovided/scope/dependency!--用于编译jsp --dependencygroupIdorg.apache.tomcat.embed/groupIdartifactIdtomcat-embed-jasper/artifactIdscopeprovided/scope/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.0/version/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdversion5.1.47/version/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins/build/project
spring 容器配置
SpringBoot工程启动会自动扫描启动类所在包下的所有Bean加载到spring容器。 1)Spring Boot配置文件 在resources下添加application.properties内容如下
server.port8080
server.servlet.context-path/security-spring-boot
spring.application.name security-spring-boot2)Spring Boot启动类
package com.mry.security.securityspringboot;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;SpringBootApplication
public class SecuritySpringBootApplication {public static void main(String[] args) {SpringApplication.run(SecuritySpringBootApplication.class, args);}}Servlet Context配置
由于Spring Boot starter自动装配机制这里无需使用EnableWebMvc与ComponentScanWebConfig如下
package com.mry.security.securityspringboot.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;Configuration
public class WebConfig implements WebMvcConfigurer {//默认Url根路径跳转到/login此url为spring security提供Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController(/).setViewName(redirect:/login);}}视图解析器配置在application.properties中
spring.mvc.view.prefix/WEB-INF/view/
spring.mvc.view.suffix.jsp安全配置
由于Spring boot starter自动装配机制这里无需使用EnableWebSecurityWebSecurityConfig内容如下
package com.mry.security.securityspringboot.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {//内容跟Spring security入门程序一致//配置用户信息服务Beanpublic UserDetailsService userDetailsService() {InMemoryUserDetailsManager manager new InMemoryUserDetailsManager();manager.createUser(User.withUsername(zhangsan).password(123).authorities(p1).build());manager.createUser(User.withUsername(lisi).password(456).authorities(p2).build());return manager;}Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}//配置安全拦截机制Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers(/r/r1).hasAuthority(p1).antMatchers(/r/r2).hasAuthority(p2).antMatchers(/r/**).authenticated() //1.anyRequest().permitAll() //2.and().formLogin().successForwardUrl(/login-success); //3}}
测试
LoginController的内容同Spring security入门程序一样。 如下
package com.mry.security.securityspringboot.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;RestController
public class LoginController {RequestMapping(value /login-success,produces {text/plain;charsetUTF-8})public String loginSuccess(){return 登录成功;}/*** 测试资源1* return*/GetMapping(value /r/r1,produces {text/plain;charsetUTF-8})public String r1(){return 访问资源1;}/*** 测试资源2* return*/GetMapping(value /r/r2,produces {text/plain;charsetUTF-8})public String r2(){return 访问资源2;}}测试过程
1、测试认证
2、测试退出
3、测试授权
工作原理
结构总览
Spring Security所解决的问题就是安全访问控制而安全访问控制功能其实就是对所有进入系统的请求进行拦截校验每个请求是否能够访问它所期望的资源根据前边知识的学习可以通过Filter或AOP等技术来实现Spring Security对Web资源的保护是靠Filter实现的所以从这个Filter来入手逐步深入Spring Security原理。
当初始化Spring Security时会创建一个名为SpringSecurityFilterChain的Servlet过滤器类型为org.springframework.security.web.FilterChainProxy它实现了javax.servlet.Filter因此外部的请求会经过此类下图是Spring Security过滤器链结构图 FilterChainProxy是一个代理真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter同时这些Filter作为Bean被Spring管理它们是Spring Security核心各有各的职责但他们并不直接处理用户的认证也不直接处理用户的授权而是把它交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecsionManager)进行处理下图是FilterChainProxy相关类的UML图示。
Spring Security功能的实现主要是由一系列过滤器链相互配合完成。
下面介绍过滤器链中主要的几个过滤器及作用 SecurityContextPersistenceFilter这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器)会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext然后把它设置给SecurityContextHolder在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository同时清除SecurityContextHolder所持有的SecurityContext
UsernamePasswordAuthenticationFilter用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码其內部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler这些都可以根据需求做相关改变
FilterSecurityInterceptor是用于保护web资源的使用AccessDecisionManager对当前用户进行授权访问前面已经详细介绍过了
ExceptionTranslationFilter能够捕获来自FilterChain所有的异常并进行处理。但是它只会处理两类异常AuthenticationException 和 AccessDeniedException其它的异常它会继续抛出。
认证流程
认证流程 让我们仔细分析认证过程 1.用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到请求信息封装为Authentication通常情况下是UsernamePasswordAuthenticationToken这个实现类。 2.然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证。 3.认证成功后AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息身份信息细节信息但密码通常会被移除)Authentication实例。 4.SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication通过SecurityContextHolder.getContext().setAuthentication(…)方法设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口也是发起认证的出发点它的实现类为ProviderManager。而Spring Security支持多种认证方式因此ProviderManager维护着一个List列表存放多种认证方式最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider它的内部又维护着一个UserDetailsService负责UserDeatils的获取。最终AuthenticationProvider将UserDetails填充至Authentication。
认证核心组件的大体关系如下
AuthenticationProvider
通过前面的Spring Security认证流程我们得知认证管理器(AuthenticationManager)委托AuthenticationProvider完成认证工作。
AuthenticationProvider是一个接口定义如下
public interface AuthenticationProvider{Authentication authenticate(Authentication authentication) throws AuthenticationException;boolean supports(Class? var1);
}authenticate()方法定义了认证的实现过程它的参数是一个Authentication里面包含了登录用户所提交的用户、密码等。而返回值也是一个Authentication这个Authentication则是在认证成功后将用户的权限及其它信息重新组装后生成。
Spring Security中维护着一个List列表存放多种认证方式不同的认证方式使用不同的AuthenticationProvider。如果使用用户名密码登录时使用AuthenticationProvider1短信登录时使用AuthenticationProvider2等等这样的例子很多。
每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式如我们使用表单方式认证在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken它是一个Authentication里面封装着用户名、密码信息。而对应的哪个AuthenticationProvider来处理它
我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现以下代码
public boolean supports(Class? authentication){return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}也就是说当web表单提交用户名密码时Spring Security由DaoAuthenticationProvider处理。
最后我们来看一下Authentication(认证信息)的结构它是一个接口我们之前提到的UsernamePasswordAuthenticationToken就是它的实现之一
public interface Authentication extends Principal, Serializable { 1Collection? extends GrantedAuthority getAuthorities(); 2Object getCredentials(); 3Object getDetails(); 4 Object getPrincipal(); 5boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException;
}(1) Authentication是Spring Security包中的接口直接继承自Principal类而Principal是位于java.security包中的它是表示着一个抽象主体身份任何主体都有一个名称因此包含一个getName()方法。 (2)getAuthorities()权限信息列表默认是GrantedAuthority接口的一些实现类通常是代表权限信息的一系列字符串。 (3)getCredentials()凭证信息用户输入的密码字符串在认证过后通常会被移除它记录了访问者的ip地址和session的值。 (4)getDetails()细节信息web应用中的实现接口通常为WebAuthenticationDetails它记录了访问者的ip地址和session的值。 (5)getPrincipal()身份信息大部分情况下返回的是UserDetails接口的实现类UserDetails代表用户的详细信息从Authentication中取出来的UserDetails就是当前登录用户信息它也是框架中的常用接口之一。
UserDetailsService
1)认识UserDetailsService 现在咱们知道DaoAuthenticationProvider处理了web表单的认证逻辑认证成功后得到一个Authenticaiton(UsernamePasswordAuthenticaionToken实现)里面包含了身份信息(Principal)。这个身份信息就是一个Object大多数情况下它可以被强制转为UserDetails对象。
DaoAuthenticationProvider中包含了一个UserDetailsService实例它负责根据用户名提取用户信息UserDetails(包含密码)而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据因此可以通过将自定义的UserDetailsService公开为Spring Bean来自定义身份验证。
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息仅此而已。而DaoAuthenticaionProvider的职责更大它完成完整的认证流程同时会把UserDetails填充至Authenticaiton。
上面一直提到UserDetails是用户信息咱们看一下它的真面目
public interface UserDetails extends Serializable {Collection? extends GrantedAuthority getAuthorities();String getPassword();String getUsername();boolean isAccountNonExpired();boolean isAccountNonLocked();boolean isCredentialsNonExpired();boolean isEnabled();
}它和Authentication接口很类似比如它们都拥有usernameauthoritiesAuthentication的getCredentials()与UserDetails中的getPassword()需要被区分对待前者是用户提交的密码凭证后者是用户实际存储的密码认证其实就是对这两者的比对。Authentication的getAuthorities()实际是由UserDetails的getAutheorities()传递而形成的。还记得Authentication接口中的getDetails()方法吗其中的UserDetails用户详细信息便是经过了AuthenticationProvider认证之后被填充的。
通过实现UserDetailsService和UserDetails我们可以完成对用户信息获取方式以及用户信息字段的扩展。 Spring Security提供的InMemoryUserDetailsManager(内存认证)JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类主要区别无非就是从内存还是从数据库加载用户、
2)测试 自定义UserDeatilsService
Service
public class SpringDataUserDetailsService implements UserDetailsService { Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //登录账号 System.out.println(usernameusername); //根据账号去数据库查询... //这里暂时使用静态数据 UserDetails userDetails User.withUsername(username).password(123).authorities(p1).build(); return userDetails; }
} 屏蔽安全配置类中UserDetailsService的定义
/*
Bean
public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager new InMemoryUserDetailsManager(); manager.createUser(User.withUsername(zhangsan).password(123).authorities(p1).build()); manager.createUser(User.withUsername(lisi).password(456).authorities(p2).build());return manager;
}*/重启工程请求认证SpringDataUserDetailsService的loadUserByUsername方法被调用查询用户信息。
PasswordEncoder
1)认识PasswordEncoder DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDedtails后它是如何与请求Authentication中的密码做对比呢
在这里Spring Security为了适应多种多样的加密类型又做了抽象DaoAuthenticationProvider通过PasswordEncoder接口的matches方法进行密码的对比而具体的密码对比细节取决于实现
public interface PasswordEncoder {String encode(CharSequence var1);boolean matches(CharSequence var1, String var2);default boolean upgradeEncoding(String encodedPassword) {return false;}
}而Spring Security提供很多内置的PasswordEncoder能够开箱即用使用某种PasswordEncoder只需要进行如下声明即可如下
Bean
public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance();
} NoOpPasswordEncoder采用字符串匹配方法不对密码进行加密比较处理密码比较流程如下 1、用户输入密码(明文) 2、DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码) 3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验密码一致则校验通过否则校验失败。 NoOpPasswordEncoder的校验规则拿输入的密码和UserDetails中的正确密码进行字符串比较字符串内容一致则校验通过否则校验失败。
实际项目中推荐使用BCryptPasswordEncoderPbkdf2PasswordEncoderScryptPasswordEncoder等感兴趣的大家可以看看这些PasswordEncoder的具体实现。
2)使用BCryptPasswordEncoder 1、配置BCryptPasswordEncoder 在安全配置类中定义
Bean
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();
}测试发现认证失败提示Encoded password does not look like BCrypt。 原因 由于UserDetails中存储的是原始密码(比如123)它不是BCrypt格式。 跟踪DaoAuthenticationProvider第33行代码查看userDetails中的内容跟踪第38行代码查看PasswordEncoder的类型。
2、测试BCrypt 通过下边的代码测试BCrypt加密及校验的方法 添加依赖
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope
/dependency编写测试方法
RunWith(SpringRunner.class)
public class TestBCrypt { Test public void test1(){ //对原始密码加密 String hashpw BCrypt.hashpw(123,BCrypt.gensalt()); System.out.println(hashpw); //校验原始密码和BCrypt密码是否一致 boolean checkpw BCrypt.checkpw(123, $2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm); System.out.println(checkpw); }
}3、修改安全配置类 将UserDetails中的原始密码修改为BCrypt格式
manager.createUser(User.withUsername(zhangsan).password($2a$10$1b5mIkehqv5c4KRrX9bUj.A4Y2hug3IGCnMCL5i4RpQrYV12xNKye).authorities(p1).build()); 实际项目中存储在数据库中的密码并不是原始密码都是经过加密处理的密码。
授权流程
授权流程
通过快速上手我们知道Spring Security可以通过http.authorizeRequests()对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截最终实现对资源的授权访问。
Spring Security的授权流程如下
分析授权流程 1.拦截请求已认证用户访问受保护的web资源将被SecurityFilterChain中的FilterSecurityInterceptor的子类拦截。 2.获取资源访问策略FilterSecurityInterceptor会从SecurityMetadataSource的子类DefaultFilterInvocationSecurityMetadataSource获取要访问当前资源所需要的权限Collection。 SecurityMetadataSource其实就是读取访问策略的抽象而读取的内容其实就是我们配置的访问规则读取访问策略如
http.authorizeRequests() .antMatchers(/r/r1).hasAuthority(p1) .antMatchers(/r/r2).hasAuthority(p2)...3.最后FilterSecurityInterceptor会调用AccessDecisionManager进行授权决策若决策通过则允许访问资源否则将禁止访问。 AccessDecisionManager(访问决策管理器)的核心接口如下
public interface AccessDecisionManager {/*** 通过传递的参数来决定用户是否有访问对应受保护资源的权限*/void decide(Authentication authentication , Object object, CollectionConfigAttribute configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;//略..
}这里着重说明一下decide的参数 authentication要访问资源的访问者的身份 object要访问的受保护资源web请求对应FilterInvocation configAttributes是受保护资源的访问策略通过SecurityMetadataSource获取。 decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
授权决策
AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。 通过上图可以看出AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权限访问受保护对象进行投票AccessDecisionManager根据投票结果做出最终决策。
AccessDecisionVoter是一个接口其中定义有三个方法具体结构如下所示。
public interface AccessDecisionVoterS {int ACCESS_GRANTED 1;int ACCESS_ABSTAIN 0;int ACCESS_DENIED ‐1;boolean supports(ConfigAttribute var1);boolean supports(Class? var1);int vote(Authentication var1, S var2, CollectionConfigAttribute var3);
}vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意ACCESS_DENIED表示拒绝ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。
SpringSecurity内置了三个基于投票的AccessDecisionManager实现类如下它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。 AffirmativeBased的逻辑是 (1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问 (2)如果全部弃权也表示通过 (3)如果没有一个人投赞成票但是有人投反对票则将抛出AccessDeniedException。
Spring Security默认使用的是AffirmativeBased。
ConsensusBased的逻辑是 (1)如果赞成票多于反对票则表示通过。 (2)反过来如果反对票多于赞成票则将抛出AccessDeniedException。 (3)如果赞成票与反对票相同且不等于0并且属性allowIfEqualGrantedDeniedDecisions的值为true则表示通过否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的默认值为true。 (4)如果所有的AccessDecisionVoter都弃权了则将视参数allowIfAllAbstainDecisions的值而定如果该值为true则表示通过否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。
UnanimousBased的逻辑与另外两种实现有点不一样另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票但是放到UnanimousBased中其投票结果就不一定是赞成了。 UnanimousBased的逻辑具体来说是这样的 (1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了则将抛出AccessDeniedException。 (2)如果没有反对票但是有赞成票则表示通过。 (3)如果全部弃权了则将视参数allowIfAllAbstainDecisions的值而定true则通过false则抛出AccessDeniedException。
SpringSecurity也内置一些投票者实现类如RoleVoter、AuthenticatedVoter和WebExpressionVoter等。
自定义认证
Spring Security提供了非常好的认证扩展方法比如快速上手中将用户信息存储到内存中实际开发中用户信息通常在数据库Spring Security可以实现从数据库读取用户信息Spring Security还支持多种授权方法。
自定义登录页面
在快速上手中你可能会想知道登录页面从哪里来的因为我们并没有提供任何的HTML或JSP文件。Spring Security的默认配置没有明确设定一个登录页面的URL因此Spring Security会根据启用的功能自动生成一个登录页面URL并使用默认URL处理登录的提交内容登录后跳转到默认URL等等。尽管自动生成的登录页面很方便快速启动和允许但大多数应用程序都希望定义自己的登录页面。
认证页面
将security-springmvc工程的login.jsp拷贝到security-springboot下目录保持一致。
配置认证页面
在WebConfig.java中配置默认认证页面地址
//默认Url根路径跳转到/login此url为spring security提供
Override
public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/).setViewName(redirect:/login‐view); registry.addViewController(/login‐view).setViewName(login);
}安全配置
在WebSecurityConfig中配置表单登录信息
//配置安全拦截机制
Override
protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/r/**).authenticated() .anyRequest().permitAll() .and() .formLogin() (1).loginPage(/login‐view) (2).loginProcessingUrl(/login) (3).successForwardUrl(/login‐success) (4).permitAll();
} (1)允许表单登录 (2)指定我们自己的登录页,spring security以重定向方式跳转到/login-view (3)指定登录处理的URL也就是用户名、密码表单提交的目的路径 (4)指定登录成功后的跳转URL (5)我们必须允许所有用户访问我们的登录页例如为验证的用户这个formLogin().permitAll()任意用户访问基于表单登录的所有的URL。
测试
当用户没有认证时访问系统的资源会重定向到login-view页面:
输入账号和密码点击登录,报错
问题解决 spring security为防止CSRFCross-site request forgery跨站请求伪造的发生限制了除了get以外的大多数方法。 解决方法1 屏蔽CSRF控制即spring security不再限制CSRF。 配置WebSecurityConfig
Override
protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() //屏蔽CSRF控制即spring security不再限制CSRF ...
}本案例采用方法1
解决方法2 在login.jsp页面添加一个tokenspring security会验证token如果token合法则可以继续请求。修改login.jsp
form actionlogin methodpostinput typehidden name${_csrf.parameterName} value${_csrf.token}/ ...
/form连接数据库认证
前边的例子我们是将用户信息存储在内存中实际项目中用户信息存储在数据库中本节实现从数据库读取用户信息。根据前边对认证流程研究只需要重新定义UserDetailService即可实现根据用户账号查询数据库。
创建数据库
创建user_db数据库
CREATE DATABASE user_db CHARACTER SET utf8 COLLATE utf8_general_ci;创建t_user表
CREATE TABLE t_user (id bigint(20) NOT NULL,username varchar(64) NOT NULL,password varchar(64) NOT NULL,fullname varchar(255) NOT NULL,mobile varchar(11) DEFAULT NULL,PRIMARY KEY (id),UNIQUE KEY uni_idx_t_user_username (username) USING BTREE
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;代码实现
1)定义dataSource 在application.properties配置
spring.datasource.urljdbc:mysql://localhost:3306/user_db
spring.datasource.usernameroot
spring.datasource.passwordroot
spring.datasource.driver‐class‐namecom.mysql.jdbc.Driver 2)添加依赖
dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope
/dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId
/dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version
/dependency3)定义Dao 定义模型类型在model包定义UserDto
Data
public class UserDto { private String id; private String username; private String password; private String fullname; private String mobile;
}在Dao包定义UserDao
Repository
public class UserDao { Autowired JdbcTemplate jdbcTemplate; public UserDto getUserByUsername(String username){ String sql select id,username,password,fullname from t_user where username ?; ListUserDto list jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper(UserDto.class)); if(list null list.size() 0){ return null; } return list.get(0); }} 定义UserDetailService
在service包下定义SpringDataUserDetailsService
Service
public class SpringDataUserDetailsService implements UserDetailsService { Autowired UserDao userDao;Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //登录账号 System.out.println(usernameusername); //根据账号去数据库查询... UserDto user userDao.getUserByUsername(username); if(user null){ return null; }//这里暂时使用静态数据 UserDetails userDetails User.withUsername(user.getFullname()).password(user.getPassword()).authorities(p1).build(); return userDetails; }
}测试
输入账号和密码请求认证跟踪代码。
使用BCryptPasswordEncoder
按照我们前边讲的PasswordEncoder的使用方法使用BCryptPasswordEncoder需要完成如下工作 1.在安全配置类中定义BCryptPasswordEncoder
Bean
public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();
} 2.UserDetails中的密码存储BCrypt格式 前边实现了从数据库查询用户信息所以数据库中的密码应该存储BCrypt格式
会话
用户认证通过后为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理认证通过后将身份信息放入SecurityContextHolder上下文SecurityContext与当前线程进行绑定方便获取用户身份。
获取用户身份
编写LoginController实现/r/r1、/r/r2的测试资源并修改loginSuccess方法注意getUsername方法Spring Security获取当前登录用户信息的方法为SecurityContextHolder.getContext().getAuthentication()
RestController
public class LoginController {/*** 用户登录成功* return*/RequestMapping(value /login‐success,produces {text/plain;charsetUTF‐8})public String loginSuccess(){String username getUsername();return username 登录成功;}/*** 获取当前登录用户名* return*/private String getUsername(){Authentication authentication SecurityContextHolder.getContext().getAuthentication();if(!authentication.isAuthenticated()){return null;}Object principal authentication.getPrincipal();String username null;if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {username ((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();} else {username principal.toString();}return username;}/*** 测试资源1* return*/GetMapping(value /r/r1,produces {text/plain;charsetUTF‐8})public String r1(){String username getUsername();return username 访问资源1;}/*** 测试资源2* return*/GetMapping(value /r/r2,produces {text/plain;charsetUTF‐8})public String r2(){String username getUsername();return username 访问资源2;}}测试 登录前访问资源 被重定向至登录页面。 登录后访问资源 成功访问资源如下
会话控制
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互
机制描述always如果没有session存在就创建一个ifRequired如果需要就创建一个Session默认登录时neverSpringSecurity 将不会创建Session但是如果应用中其他地方创建了Session那么Spring Security将会使用它。statelessSpringSecurity将绝对不会创建Session也不使用Session
通过以下配置方式对该选项进行配置
Override
protected void configure(HttpSecurity http) throws Exception {http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
}默认情况下Spring Security会为每个登录成功的用户会新建一个Session就是ifRequired 。
若选用never则指示Spring Security对登录成功的用户不创建Session了但若你的应用程序在某地方新建了session那么Spring Security会用它的。
若使用stateless则说明Spring Security对登录成功的用户不会创建Session了你的应用程序也不会允许新建session。并且它会暗示不使用cookie所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API及其无状态认证机制。
会话超时
可以再sevlet容器中设置Session的超时时间如下设置Session有效期为3600s spring boot 配置文件
server.servlet.session.timeout3600ssession超时之后可以通过Spring Security设置跳转的路径。
http.sessionManagement().expiredUrl(/login‐view?errorEXPIRED_SESSION).invalidSessionUrl(/login‐view?errorINVALID_SESSION);expired指session过期invalidSession指传入的sessionid无效。
安全会话cookie
我们可以使用httpOnly和secure标签来保护我们的会话cookie
httpOnly如果为true那么浏览器脚本将无法访问cookiesecure如果为true则cookie将仅通过HTTPS连接发送
spring boot 配置文件
server.servlet.session.cookie.http-onlytrue
server.servlet.session.cookie.securetrue退出
Spring security默认实现了logout退出访问/logout果然不出所料退出功能Spring也替我们做好了。 待补充
点击“Log Out”退出 成功。 退出 后访问其它url判断是否成功退出。 这里也可以自定义退出成功的页面 在WebSecurityConfig的protected void configure(HttpSecurity http)中配置
.and()
.logout()
.logoutUrl(/logout)
.logoutSuccessUrl(/login-view?logout);当退出操作出发时将发生
使HTTP Session 无效清除SecurityContextHolder跳转到 /login-view?logout
但是类似于配置登录功能咱们可以进一步自定义退出功能
Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()//... .and().logout() (1).logoutUrl(/logout) (2).logoutSuccessUrl(/login‐view?logout) (3).logoutSuccessHandler(logoutSuccessHandler) (4).addLogoutHandler(logoutHandler) (5) .invalidateHttpSession(true); (6)}(1)提供系统退出支持使用WebSecurityConfigurerAdapter会自动被应用 (2)设置触发退出操作的URL(默认是/logout) (3)退出之后跳转的URL。默认是/login?logout。 (4)定制的LogoutSuccessHandler用于实现用户退出成功时的处理。如果指定了这个选项那么logoutSuccessUrl()的设置会被忽略。 (5)添加一个LogoutHandler用于实现用户退出时的清理工作默认SecurityContextLogoutHandler会被添加为最后一个LogoutHandler。 (6)指定是否在退出时让HttpSession无效。默认设置为true。 注意如果让logout在GET请求下生效必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF必须使用post方式请求/logout
logoutHandler 一般来说LogoutHandler的实现类被用来执行必要的清理因而他们不应该抛出异常。 下面是Spring Security提供的一些实现
PersistentTokenBasedRememberMeServices基于持久化token的RememberMe功能的相关清理TokenBasedRememberMeService基于token的RememberMe功能的相关清理CookieClearingLogoutHandler退出时Cookie的相关清理CsrfLogoutHandler负责在退出时移除csrfTokenSecurityContextLogoutHandler退出时SecurityContext的相关清理
链式API提供了调用相应的LogoutHandler实现的快捷方式比如deleteCookies()。
授权
概述
授权的方式包括web授权和方法授权web授权是通过url拦截进行授权方法授权是通过 方法拦截进行授权。他们都会调用accessDecisionManager进行授权决策若为web授权则拦截器为FilterSecurityInterceptor若为方法授权则拦截为MethodSecurityInterceptor。如果同时通过web授权和方法授权则先执行web授权再执行方法授权最后决策通过则允许访问资源否则将禁止访问。
类关系如下
准备环境
数据库环境
在t_user数据库创建如下表 角色表
CREATE TABLE t_role (id varchar(32) NOT NULL,role_name varchar(64) DEFAULT NULL,description varchar(255) DEFAULT NULL,create_time datetime DEFAULT NULL COMMENT 创建时间,update_time datetime DEFAULT NULL COMMENT 修改时间,status char(1) DEFAULT NULL,PRIMARY KEY (id),UNIQUE KEY uni_idx_t_role_role_name (role_name)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;INSERT INTO user_db.t_role (id, role_name, description, create_time, update_time, status) VALUES (1, 管理员, NULL, 2020-11-14 15:16:22, 2020-11-14 15:16:22, NULL);用户角色关系表
CREATE TABLE t_user_role (user_id varchar(32) NOT NULL,role_id varchar(32) NOT NULL,create_time datetime DEFAULT NULL COMMENT 创建时间,creator varchar(255) DEFAULT NULL,PRIMARY KEY (user_id)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;INSERT INTO user_db.t_user_role (user_id, role_id, create_time, creator) VALUES (1, 1, 2020-11-14 15:12:21, NULL);权限表
CREATE TABLE t_permission (id varchar(32) NOT NULL,code varchar(32) NOT NULL,description varchar(255) NOT NULL,url varchar(255) NOT NULL,PRIMARY KEY (id)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;INSERT INTO user_db.t_permission (id, code, description, url) VALUES (1, p1, 测试资源1, /r/r1);
INSERT INTO user_db.t_permission (id, code, description, url) VALUES (2, p2, 测试资源2, /r/r2);
INSERT INTO user_db.t_permission (id, code, description, url) VALUES (3, p3, 测试资源2, /r/r2);角色权限关系表
CREATE TABLE t_role_permission (role_id varchar(32) NOT NULL,permission_id varchar(32) NOT NULL
) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色(role)权限(permission)关系表;INSERT INTO user_db.t_role_permission (role_id, permission_id) VALUES (1, 1);
INSERT INTO user_db.t_role_permission (role_id, permission_id) VALUES (1, 3);修改UserDetailService
1.修改dao接口 在UserDao中添加
//根据用户id查询用户权限
public ListString findPermissionsByUserId(String userId){ String sqlSELECT * FROM t_permission WHERE id IN(\n SELECT permission_id FROM t_role_permission WHERE role_id IN(\n \tSELECT role_id FROM t_user_role WHERE user_id ? \n )\n ); ListPermissionDto list jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper(PermissionDto.class)); ListString permissions new ArrayList(); list.iterator().forEachRemaining(c‐permissions.add(c.getCode())); return permissions; }2.修改UserDetailService 实现从数据库读取权限
Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //登录账号 System.out.println(usernameusername); //根据账号去数据库查询... UserDto user userDao.getUserByUsername(username); if(user null){ return null; } //查询用户权限 ListString permissions userDao.findPermissionsByUserId(user.getId()); String[] perarray new String[permissions.size()]; permissions.toArray(perarray); //创建userDetails UserDetails userDetails User.withUsername(user.getFullname()).password(user.getPassword()).authorities(perarray).build(); return userDetails;
}web授权
在上面例子中我们完成了认证拦截并对/r/**下的某些资源进行简单的授权保护但是我们想进行灵活的授权控制该怎么做呢通过给http.authorizeRequests()添加多个子节点来定制需求到我们的URL如下代码
Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests() (1).antMatchers(/r/r1).hasAuthority(p1) (2).antMatchers(/r/r2).hasAuthority(p2) (3).antMatchers(/r/r3).access(hasAuthority(p1) and hasAuthority(p2)) (4).antMatchers(/r/**).authenticated() (5).anyRequest().permitAll() (6) .and() .formLogin()// ...
}(1)http.authorizeRequests()方法有多个子节点每个macher按照他们的声明顺序执行。 (2)指定/r/r1URL拥有p1权限能够访问 (3)指定/r/r2URL拥有p2权限能够访问 (4)指定了/r/r3URL同时拥有p1和p2权限才能够访问 (5)指定了除了r1、r2、r3之外/r/**资源同时通过身份认证就能够访问这里使用SpELSpring Expression Language表达式。 (6)剩余的尚未匹配的资源不做保护。
注意 规则的顺序是重要的更具有的规则应该先写。现在以/admin开始的所有内容都需要具有ADMIN角色的身份验证用户即使是/admin/login路径(因为/admin/login已经被/admin/**规则匹配因此第二个规则被忽略)。
.antMatchers(/admin/**).hasRole(ADMIN)
.antMatchers(/admin/login).permitAll()因此,登录页面的规则应该在/ admin / **规则之前.例如
.antMatchers(/admin/login).permitAll()
.antMatchers(/admin/**).hasRole(ADMIN)保护URL常用的方法有 authenticated() 保护URL需要用户登录 permitAll() 指定URL无需保护一般应用与静态资源文件 hasRole(String role) 限制单个角色访问角色将被增加 “ROLE_” .所以”ADMIN” 将和 “ROLE_ADMIN”进行比较. hasAuthority(String authority) 限制单个权限访问 hasAnyRole(String… roles) 允许多个角色访问. hasAnyAuthority(String… authorities) 允许多个权限访问. access(String attribute) 该方法使用 SpEL表达式, 所以可以创建复杂的限制. hasIpAddress(String ipaddressExpression) 限制IP地址或子网
方法授权
现在我们已经掌握了使用如何使用http.authorizeRequests()对web资源进行授权保护从Spring Security2.0版本开始它支持服务层方法的安全性的支持。本节学习PreAuthorize,PostAuthorize, Secured三类注解。 我们可以在任何Configuration实例上使用EnableGlobalMethodSecurity注解来启用基于注解的安全性。 以下内容将启用Spring Security的Secured注释
EnableGlobalMethodSecurity(securedEnabled true)
public class MethodSecurityConfig {// ...}然后向方法在类或接口上添加注解就会限制对该方法的访问。 Spring Security的原生注释支持为该方法定义了一组属性。 这些将被传递给AccessDecisionManager以供它作出实际的决定
public interface BankService {Secured(IS_AUTHENTICATED_ANONYMOUSLY)public Account readAccount(Long id);Secured(IS_AUTHENTICATED_ANONYMOUSLY)public Account[] findAccounts();Secured(ROLE_TELLER)public Account post(Account account, double amount);}以上配置标明readAccount、findAccounts方法可匿名访问底层使用WebExpressionVoter投票器可从AffirmativeBased第23行代码跟踪。
post方法需要有TELLER角色才能访问底层使用RoleVoter投票器。
使用如下代码可启用prePost注解的支持
EnableGlobalMethodSecurity(prePostEnabled true)public class MethodSecurityConfig { // ...
}
相应Java代码如下
public interface BankService {PreAuthorize(isAnonymous())public Account readAccount(Long id);PreAuthorize(isAnonymous())public Account[] findAccounts();PreAuthorize(hasAuthority(p_transfer) and hasAuthority(p_read_account))public Account post(Account account, double amount);}以上配置标明readAccount、findAccounts方法可匿名访问post方法需要同时拥有p_transfer和p_read_account权限才能访问底层使用WebExpressionVoter投票器可从AffirmativeBased第23行代码跟踪。