Spring Security概述
1.1 Spring Security介绍
Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。
官网地址: https://projects.spring.io/spring-security/
Spring Security 为基于J2EE企业应用软件提供了全面安全服务。
特别是使用领先的J2EE解决方案-Spring框架开发的企业软件项目。人们使用Spring Security有很多种原因,不过通常吸引他们的是在J2EE Servlet规范或EJB规范中找不到典型企业应用场景的解决方案。 特别要指出的是他们不能再
WAR 或 EAR 级别进行移植。这样,如果你更换服务器环境,就要,在新的目标环境进行大量的工作,对你的应用
系统进行重新配 置安全。使用Spring Security 解决了这些问题,也为你提供很多有用的,完全可以指定的其他安
全特性。 安全包括两个主要操作。
本质:
Spring Security 采用 AOP,以及基于 Filter过滤器实现的安全认证框架。它提供了完善的认证机构和授权功能,而在做系统的时候,一般做的第一个模块就是认证与授权模块,因为这是一个系统的入口,也是一个系统最重要最基础的一环,在认证与授权服务设计搭建好了之后,剩下的模块才得以安全访问。
小结:
- springSecurity基于spring构建的安全认证服务框架
- springSecurity基于Filter实现的安全认证服务框架(必须在web项目中).
- springSecurity基于aop实现认证和授权服务的(aop底层是动态代理)
1.2 登录认证(Authentication)和访问授权(Authorization)
-
什么是登录认证
指的获取用户输入的账号和密码信息,校验是否正确,这个过程就是认证过程.
-
什么是访问授权
指的登录认证通过后, 根据用户的账号信息获取该用户所匹配的资源数据, 让用户访问与之匹配的资源.
比如: 资源数据指的用户访问的页面,或者用户访问controller里面的请求路径,执行controller里面的方法
-
**“登录认证”**指的是为用户建立一个他所声明的主体,主题一般式指用户,设备或可以在你系 统中执行动作的其他系统。
1、基于表单的认证(Cookie & Session):基于表单的认证并不是在 HTTP 协议中定义的,而是服务器自己实现的认证方式,安全程度取决于实现程度。一般用 Cookie 来管理 Session 会话,是最常用的认证方式之一。它的安全程度取决于服务器的实现程度,客户端在Cookie中携带认证信息,服务器解析并返回结果。
总结:
-
首次访问服务器端, 将用户信息 提交服务器端, 在服务器端对用户提交的信息进行认证,
如果认证通过将认证过的信息存到cookie里面,同时向cookie响应到浏览器端
比如: cookie里面存到信息: pass=“yes”
-
下次再访问时, 提交cookie到服务器端, 在服务器端获取cookie里面的信息, 判断是否已经认证,
比如: 在服务器端获取cookie里面的信息: pass="yes",说明之前认证过了,不在认证
2、基于JWT(Json Web Token)的认证:App和服务端常用的认证方式,用户ID和密码传输到服务器上验证,服务器验证通过以后生成加密的JWT Token返回给客户端,客户端再发起请求时携带返回的Token进行认证。(多了个防篡改)
总结: 基于 JWT 令牌认证(三部分数据)认证过程,进行加密处理, 将认证后的数据以json格式形式保存到token.
底层也是基于cookie实现的.
-
用户首次访问服务器端, 提交用户信息, 在服务器端进行认证, 认证通过过,将认证的信息以json格式保存到token
底层: 将token保存到cookie里面, 将cookie响应到客户端.
-
下次再次访问服务器端时, 将客户端保存的cookie提交到服务器端, 在服务器端获取cookie, 解析里面的 token.
如果token里面保存了之前的认证信息, 可以不再认证了
比如: 基于jwt进行登录认证,可以对认证数据进行加密, 可以对认证数据设置过期时间.
3、Http Basic 认证:最早的 Http 认证方式,用户 ID 和密码以分号连接,经过 Base64 编码后存储到 Authorization 字段,发送到服务端进行认证 ;用户 ID/密码 以明文形式暴露在网络上,安全性较差。(如果没有使用 SSL/TLS 这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截)
4、Http Digest 认证:在 HttpBasic 的基础上,进行了一些安全性的改造,用户ID, 密码 , 服务器/客户端随机数,域,请求信息,经过 MD5 加密后存储到 Authorization 字段,发送到服务端进行认证;密码经过 MD5 加密,安全性比 Basic 略高。
5、其他认证方式(Oauth 认证,单点登陆,HMAC 认证):通过特定的加密字段和加密流程,对客户端和服务端的信息进行加密生成认证字段,放在 Authorization 或者是消息体里来实现客户信息的认证
-
Oauth认证: 基于第三方进行认证, 比如: 登录csdn可以使用第三方账号,比如: 微信登录.
-
单点登录: 一次登录,到处通行(指的登录一个网站,可以访问与之相关的其它网站,这时在访问其它网站时,不在认证)
比如: 比如登录淘宝以后, 再访问天猫,或者聚划算, 或者 阿里云 不用再登录.
单点登录的实现方式:
比如基于cas进行单点登录: client端---------------搭建的单点登录服务器.
-
第一次访问淘宝, 输入账号信息, 将账号信息提交到单点登录服务器 进行认证,认证通过以后, 将认证通过后的信息存到cookie里面.
-
下次再访问 淘宝时,提供cookie到单点登录服务器, 获取cookie.解析认证信息,如果之前认证过了 , 不再认证.
-
下次再访问天猫时, 如果能提交之前保存认证信息的cookie到单点登录服务器, 直接认证通过.
如果不能提交之前保存认证信息的cookie, 必须再次认证.
-
-
-
“访问授权” 指的是一个用户能否在你的应用中执行某个操作,在到达授权判断之前,身份的主题已经由 身份验证
过程建立了。
这些概念是通用的,不是Spring Security特有的。在身份验证层面,Spring Security广泛支持各种身份验证模式,
这些验证模型绝大多数都由第三方提供,或则正在开发的有关标准机构提供的,
例如 Internet Engineering TaskForce.作为补充,
Spring Security 也提供了自己的一套验证功能。
Spring Security 目前支持认证一体化如下认证技术:
HTTP BASIC authentication headers (一个基于IEFT RFC 的标准)
HTTP Digest authentication headers (一个基于IEFT RFC 的标准)
HTTP X.509 client certifificate exchange(一个基于IEFT RFC 的标准)
LDAP (一个非常常见的跨平台认证需要做法,特别是在大环境)
Form-based authentication (提供简单用户接口的需求)
OpenID authentication Computer Associates Siteminder JA-SIG Central Authentication Service (CAS,这是一个流行的开源单点登录系统)
Transparent authentication context propagation for Remote Method Invocation and HttpInvoker (一个Spring远程调用协议)
总结:
-
认证 (Authentication): 你是谁。
Authentication(认证) 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。
-
授权 (Authorization): 你有权限干什么。
Authorization(授权) 发生在 Authentication(认证) 之后。授权嘛,光看意思大家应该就明白,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如 admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。
如图:
1.3 认证流程
这里的 AuthenticationFilter 采用的是责任链设计模式(请求层层上报,直到有人解决为止),一个 web 请求会经过一条过滤器链,在经过过滤器链的过程中会完成认证与授权,如果中间发现这条请求未认证或者未授权,会根据被保护 API 的权限去抛出异常,然后由异常处理器去处理这些异常。
FilterchainProxy过滤链List如下图所示
注:这里 Security 提供两种过滤器类:
UsernamePasswordAuthenticationFilter
表示表单登陆过滤器BasicAuthenticationFilter
表示 httpBasic 方式登陆过滤器
如上图,一个请求想要访问到 API 就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。
不过上述的两个过滤器是 Spring Security 对 form 表单认证和 Basic 认证内置的两个 Filter.
1.4 认证核心组件(api)
- SecurityContext:上下文对象,Authentication 对象会放在里面。
- SecurityContextHolder:用于拿到上下文对象的静态工具类。
- Authentication:认证接口,定义了认证对象的数据形式。
- AuthenticationManager:用于校验 Authentication,返回一个认证完成后的 Authentication 对象
SecurityContext
上下文对象,认证后的数据就放在这里面,接口定义如下:
public interface SecurityContext extends Serializable {
// 获取Authentication对象
Authentication getAuthentication();
// 放入Authentication对象
void setAuthentication(Authentication authentication);
}
SecurityContextHolder
用于拿到上下文对象的静态工具类。
SecurityContextHolder 用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限… 这些都被保存在 SecurityContextHolder 中。SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。
public class SecurityContextHolder {
public static void clearContext() {
strategy.clearContext();
}
public static SecurityContext getContext() {
return strategy.getContext();
}
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
可以说是 SecurityContext 的工具类,用于 get or set or clear SecurityContext,默认会把数据都存储到当前线程中。
从这个 SecurityContextHolder 取得 UserDetail 的实例:
public static String getLoginAccount() {
// getPrincipal 返回值需要强转为 UserDetail 具体原因看下面的 Authentication那节
return ((UserDetail) SecurityContextHolder
.getContext()
.getAuthentication() // 返回:Authentication
.getPrincipal()) // 这里就是 Authentication 内部保存的 UserDetail 对象
.getUsername();
}
Authentication
认证接口,定义了认证对象的数据形式。
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
// AuthenticationManager 实现通常会返回一个包含更丰富信息的 Authentication 作为供应用程序使用的主体。
// 许多身份验证提供程序将创建一个 UserDetails 对象作为主体
// 所以如果 AuthenticationManager 使用的是 ProviderManager 则这里返回值需要强转为 UserDetails
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
这几个方法效果如下:
- getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
- getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
- getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)。
- getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails。
- isAuthenticated: 获取当前 Authentication 是否已认证。
- setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。
Authentication 只是定义了一种在 SpringSecurity 进行认证过的数据的数据形式应该是怎么样的,要有权限,要有密码,要有身份信息,要有额外信息。
AuthenticationManager ⭐
认证管理器很多,在这里不做过多介绍
public interface AuthenticationManager {
// 认证方法
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager (接口)是认证相关的核心接口,它定义了一个认证方法,它将一个未认证的 Authentication 传入,返回一个已认证的 Authentication。它的默认实现类是 ProviderManager
注:AuthenticationManager 有多个认证类 AuthenticationManager,ProviderManager ,AuthenticationProvider …
总结:
整合上面四个组件认证流程
将这四个部分,串联起来,构成 Spring Security 进行认证的流程:
1、先是一个请求带着身份信息进来,用户名和密码被过滤器获取到,封装成 Authentication
,通常情况下是 UsernamePasswordAuthenticationToken
这个实现类。
2、这个 Authentication
经过 AuthenticationManager
的认证(身份管理器负责验证)认证成功后,AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication
实例。
3、SecurityContextHolder
安全上下文容器将上面填充了信息的 Authentication
,通过 SecurityContextHolder.getContext().setAuthentication(…)
方法,设置到 SecurityContext
其中。
下面编写一个例子,具体的描述这个流程(不是源码, 只是模拟认证流程)
public class AuthenticationExample {
private static final AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while(true) {
System.out.println("Please enter your username:");
String name = in.readLine();
System.out.println("Please enter your password:");
String password = in.readLine();
try {
// 1、封装一个 UsernamePasswordAuthenticationToken 对象
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
// 2、经过 AuthenticationManager 的认证,如果认证失败会抛出一个 AuthenticationException 错误
Authentication result = am.authenticate(request);
// 3、将这个认证过的 Authentication 填入 SecurityContext 里面
SecurityContextHolder.getContext().setAuthentication(result);
break;
} catch(AuthenticationException e) {
System.out.println("Authentication failed:" + e.getMessage());
}
}
System.out.println("Successfully authenticated. Security context contains:\n" +
SecurityContextHolder.getContext().getAuthentication());
}
}
// 实现一个简单 AuthenticationManager 用于认证
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>();
static {
AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
// 关键认证部分
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
// getCredentials 返回的是密码,这里随便写了,直接用户名和密码一致就算登陆成功
if (auth.getName().equals(auth.getCredentials())) {
// 认证成功返回一个已经认证的 UsernamePasswordAuthenticationToken 的对象,并把这个用户的权限填入
return new UsernamePasswordAuthenticationToken(auth.getName(),
auth.getCredentials(), AUTHORITIES);
}
throw new BadCredentialsException("Bad Credentials");
}
}
1.5 授权流程
授权分析
认证成功之后会将当前登录用户信息保存到Authentication对象中,Authentication对象中有一个getAuthorities()方法,用来返回当前登录用户具备的权限信息,也就是当前用户具有权限信息。该方法的返回值为Collection<? extends GrantedAuthority>,当需要进行权限判断时,就会根据集合返回权限信息调用相应 方法进行判断。
那么问题来了,针对于这个返回值GrantedAuthority应该如何理解?是角色还是权限?
基于角色进行权限管理
一般情况下: 设计权限表:
user表-user_role中间表–role角色表-----role_permission中间表 --permission权限表
实现方式:
在认证通过以后, 根据用户信息查询该用户所拥有的角色, 将用户认证信息和角色信息设置到springSecurity提供的UserDetails(比较流行)
基于资源进行权限管理 -->权限字符串
R(Role Resources) B(base) A(access) C(controll)
我们针对于授权可以是基于角色权限管理和基于资源权限管理,从设计层面上来说,角色和权限是两个完全不同的东西:权限是一些具体操作,角色则是某些权限集合。如:READ_BOOK和ROLE_ADMIN是完全不同的。因此至于返回值是什么取决于你的业务设计情况:
基于角色权限设计就是:用户<>角色<>资源 三者关系 返回就是用户的角色(用的多)
基于资源权限设计就是: 用户<>权限<>资源 三者关系 返回就是用户的权限
基于角色和资源权限设计就是: 用户<>角色<>权限<==>资源 返回统称为用户的权限
为什么统称为权限,因为从代码层面角色和权限没有太大不同都是权限,特别是在Spring Security中。角色和权限处理方式基本上都是一样的。唯一区别SpringSecurity在很多时候会自动给角色添加一个**ROLE_**前缀,而权限则不会自动添加。
权限管理策略
可以访问系统中哪些资源(http url method)
Spring Security中提供的权限管理策略主要有两种类型:
- 基于过滤器(URL)的权限管理(FilterSecurityInterceptor)
基于过滤器的权限管理主要用来拦截HTTP请求,拦截下来之后,根据HTTP请求地址进行权限校验。 - 基于AOP的权限管理(MethodSecurityInterceptor)
基于AOP权限管理主要是用来处理方法级别的权限问题。当需要调用某一个方法时,通过AOP将操作拦截下来,然后判断用户是否具备相关的权限。
基于url的权限管理代码如下
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//创建内存数据源
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("root").password("{noop}123").roles("ADMIN","USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("lisi").password("{noop}123").roles("USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("win7").password("{noop}123").authorities("READ_INFO").build());
return inMemoryUserDetailsManager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/admin").hasRole("ADMIN") //具有 admin角色
.mvcMatchers("/user").hasRole("USER") //具有user角色
.mvcMatchers("/getInfo").hasAuthority("READ_INFO") //具有read_info权限
.antMatchers(HttpMethod.GET,"admin").hasRole("ADMIN")
// .regexMatchers().hasRole() //好处: 支持正则表达式
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable();
}
}
权限表达式
基于方法的权限管理
基于方法的权限管理主要是通过AOP来实现的,Spring Security中通过MethodSecurityInterceptor来提供相关的实现。不同在于FilterSecurityInterceptor只是在请求之前进行前置处理,MethodSecurityInterceptor除了前置处理之外还可以进行后置处理。前置处理就是在请求之前判断是否具备相应的权限,后置处理则是对方法的执行结果进行二次过滤。前置处理和后置处理分别对应了不同的实现类。
@EnableGlobalMethodSecurity
EnableGlobalMethodSecurity该注解是用来开启权限注解,用法如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true,jsr250Enabled=true)
public class SecurityConfig extends WebsecurityConfigurerAdapter{
}
perPostEnabled:开启Spring Security提供的四个权限注解,
@PostAuthorize、@PostFilter、@PreAuthorize以及@PreFilter。
securedEnabled:开启Spring Security提供的@Secured注解支持,该注解不支持权限表达式
jsr250Enabled:开启JSR-250提供的注解,主要是@DenyAll、@PermitAll、@RolesAll同样这些注解也不支持权限表达式
以上注解含义如下:
- @PostAuthorize: 在目标方法执行之后进行权限校验。
- @PostFilter: 在目标方法执行之后对方法的返回结果进行过滤。
- @PreAuthorize: 在目标方法执行之前进行权限校验。
- @PreFilter: 在目标方法执行之前对方法参数进行过滤。
- @secured: 访问目标方法必须具备相应的角色。
- @DenyAll: 拒绝所有访问。
- @PermitAll: 允许所有访问。
- @RolesAllowed: 访问目标方法必须具备相应的角色。
这些机遇方法的权限管理相关的注解,一般来说只要设置prePostEnabled=true就够用了。
如图配置:
代码如下
@RestController
@RequestMapping("/hello")
public class AuthorizeMethodController {
@PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")
@GetMapping
public String hello(){
return "hello";
}
@PreAuthorize("authentication.name ==#name") //参数与认证的用户名一致才可以访问
@GetMapping("/name")
public String hello(String name){
return "hello:" + name;
}
@PreFilter(value ="filterObject.id%2!=0",filterTarget = "users") //filterTarget表示要过滤的参数,必须是数组、集合类型 filterObject是数组中的对象,如User。
@PostMapping("/users")
public void addUsers(@RequestBody List<User> users){
System.out.println("users = "+ users);
}
@PostAuthorize("returnObject.id ==1") //表示方法返回前处理
@GetMapping("/userId")
public User getUserById(Integer id){
return new User(id,"zkt");
}
@PostFilter("filterObject.id%2==0") //用来对方法返回值进行过滤
@GetMapping("/lists")
public List<User> getAll(){
List<User> users = new ArrayList<>();
for(int i = 0; i < 10; i++){
users.add(new User(i,"zkt"+i));
}
return users;
}
@Secured({"ROLE_USER"}) //只能判断角色
@GetMapping("/secured")
public User getUserByUsername(){
return new User(99,"secured");
}
@Secured({"ROLE_ADMIN","ROLE_USER"}) //具有其中一个即可
@GetMapping("/username")
public User getUserByUsername2(String username){
return new User(99,username);
}
@PermitAll
@GetMapping("/permitAll")
public String permitAll(){
return "PermitAll";
}
@DenyAll
@GetMapping("/denyAll")
public String denyAll(){
return "DenyAll";
}
@RolesAllowed({"ROLE_ADMIN","ROLE_USER"}) //具有其中一个角色即可
@GetMapping("/rolesAllowed")
public String rolesAllowed(){
return "RolesAllowed";
}
}
原理:
-
ConfigAttribute 在Sring Security中,用户请求一个资源(通常是一个接口或者一个Java 方法)需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute方法,该方法返回一个String字符串,就是角色的名称。一般来说,角色名称都带有一个ROLE_ 前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具有的各个角色和请求某个资源所需的ConfigAttribue之间的关系。
-
AccessDecisionVoter和AccessDecisionManager都有众多的实现类,在AccessDecisionManager中会逐个遍历AccessDecisionVoter,进而决定是否允许用户访问,因而AccessDecisionVoter和AccessDecisionManager两者的关系类似于AuthenticationProvider和ProviderManager的关系。
-
小结
-
基于url授权
访问目标资源之前,进行权限校验.
-
基于方法授权(web层controller类里面的方法)
访问目标方法之前,进行权限校验.
-
jsr250: 是有java提供的授权方式
-
springsecurity自身提供的基于方法的授权
-
springSecurity和shiro两个权限校验框架最大的不同
-
springSecurity必须在web开发环境里面使用.
-
shiro可以用在任意项目里面
-
-
-
1.6 springSecurity入门
-
步骤一: 导入springSecurity依赖以及相关依赖
<properties> <spring.security.version>5.0.1.RELEASE</spring.security.version> </properties> <!-- web 工程必备,包含过滤器和相关的 Web 安全基础结构代码--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${spring.security.version}</version> </dependency> <!--用于解析 xml 配置文件--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${spring.security.version}</version> </dependency> <!--任何 Spring Security 功能都需要此包--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>${spring.security.version}</version> </dependency> <!--提供的动态标签库,jsp 页面可以用--> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>${spring.security.version}</version> </dependency>
-
步骤二: 配置spring-security.xml文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <!-- 配置Spring Security auto-config="true",如果没有认证,第一次自动生成登录页面,没有认证通过,自动提示错误信息等等 use-expressions="true"表示使用spring的el表达式来配置spring security --> <security:http auto-config="true" use-expressions="false"> <!-- intercept-url定义一个过滤规则 pattern表示对哪些url进行权限控制,access属性表示在请求对应 的URL时需要什么权限, 默认配置时它应该是一个以逗号分隔的角色列表,请求的用户只需拥有其中的一个角色就能成功访问对应 的URL --> <security:intercept-url pattern="/**" access="ROLE_USER" /> <!-- auto-config配置后,不需要在配置下面信息 <security:form-login /> 定义登录表单信息 <security:http-basic /> <security:logout /> --> </security:http> <!-- (noop: no operation) 不加密算法--> <security:authentication-manager> <security:authentication-provider> <security:user-service> <security:user name="user" password="{noop}user" authorities="ROLE_USER" /> <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" /> </security:user-service> </security:authentication-provider> </security:authentication-manager> </beans>
-
步骤三:配置web.xml
如果有spring的配置文件,可以将spring-security的配置文件引入到spring的配置文件里面去
<!--在spring的配置文件里面,引入SpringSecurity主配置文件--> <import resource="classpath:spring-security.xml"/>
如果没有spring的配置文件,单独加载
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-security2.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <!--固定的springSecurityFilterChain--> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> </web-app>
当我们访问index.html页面时发现会弹出登录窗口,可能你会奇怪,我们没有建立下面的登录页面,为什么Spring
Security会跳到上面的登录页面呢?这是我们设置http的auto-config=”true”时Spring Security自动为我们生成的。
1.7 入门自定义页面
-
步骤一:导入依赖省略
-
步骤二: 修改spring-security.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <!-- 配置不过滤的资源(静态资源及登录相关) --> <security:http security="none" pattern="/login.html" /> <security:http security="none" pattern="/failer.html" /> <security:http auto-config="true" use-expressions="false" > <!-- 配置资料连接,表示任意路径都需要ROLE_USER权限 --> <security:intercept-url pattern="/**" access="ROLE_USER" /> <!-- 自定义登陆页面,login-page 自定义登陆页面 authentication-failure-url 用户权限校验失败之后才会跳转到这个页面,如果数据库中没有这个用户则不会跳转到这个页面。 default-target-url 登陆成功后跳转的页面。 注:登陆页面用户名固定 username,密码 password,action:login --> <security:form-login login-page="/login.html" login-processing-url="/login" username-parameter="username" password-parameter="password" authentication-failure-url="/failer.html" default-target-url="/success.html" authentication-success-forward-url="/success.html" /> <!-- 关闭CSRF(跨域访问),默认是开启的,这里为true是设置关闭 --> <security:csrf disabled="true" /> </security:http> <security:authentication-manager> <security:authentication-provider> <security:user-service> <security:user name="user" password="{noop}user" authorities="ROLE_USER" /> <security:user name="admin" password="{noop}admin" authorities="ROLE_ADMIN" /> </security:user-service> </security:authentication-provider> </security:authentication-manager> </beans>
1.8 springSecurity认证使用数据库认证
- 没有springSecurity的登录流程图:
没有springSecurity框架, 实现认证的流程
- 访问login.jsp登录页面, 在登录页面输入用户名和密码,提交到userControleller
- UserController里面: 获取用户信息, 调用注入的Service层的对象去查询用户信息
- UserService里面: 调用注入UserMapper,根据用户和密码查询sys_user表
UserController ---->UserService-------------->UserMapper (根据用户名和密码去查询sys_user表,查询出来了,封装UserInfo)
在service层拿到dao层 返回的 UserInfo.
如果userInfo==null, 说明根据用户名和密码没有找到用户, 认证不通过,反之认证通过.
优点:
- 认证流程简单清晰
缺点:
没有实现授权操作,我们手动实现:
- 根据用户名查询出该用户匹配的角色,在根据角色查询所对应的权限资源
- 手动授权时机: 方式一: 在过滤器里面授权, 方式二: 基于aop授权.
- 基于springSecurity的认证和授权流程图
回顾:
-
springSecurity是基于角色进行授权(手动封装数据到UserDetails.)
实现的原理: 将认证通过的信息,以及该用户的角色信息保存到springSecurity提供的: UserDetails.
-
springSecurity怎么授权的(自动实现)
底层: UserDetails 根据自身的service, 拿到UserDetails里面的角色数据,查询该角色与之匹配的权限资源.
问题:
-
如何将查询出来的UserInfo数据封装到springSecurity提供的 UserDetails
-
如何根据用户信息查询与用户匹配的角色数据 ,封装到UserDetails
UserDetails是一个接口, springSecurity提供了一个实现User ,封装user数据,实现封装 UserDetails
-
springSecurity提供了一个不可逆的加密算法: BCryptPasswordEncoder,加密后的数据不能再解密了
问题: 如何实现认证?
解决方案:
-
根据用户名去查询sys_user表, 查询出来加密后的密码
-
由springSecurity提供的 UserDetailsService 根据用户提交的密码 与 数据库的加密后的密码进行判断
如果用户提交的密码 经过匹配, 与加密后的密码一致, 表示认证通过
如果用户提交的密码 经过匹配, 与加密后的密码不一致, 表示认证不通过
简单分析如下:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; //666 = $2a$10$XTSjCY.IMfOi1NbKOyrDLOU11yLX.mTZIaxJ.okSHE8Ypc/Iao/ze //666 = $2a$10$zVDtFfT/RP7xDkUQ72cP0.jjAQEGAKansDdZV3RRKB/0kxh0Lf5fW /** * 不可逆的加密算法底层实现 * 原理: 在对数据进行加密前, 对被加密的数据进行了加salt处理. * 什么是salt盐呢? * String[] salts = ["aaa","bbb","ccc","ddd"-----] * * 实现: * 1. 通过一个随机数, 从数组里面获取一个salt * 2. 将salt存到某个位置 * 3. 让(salt+被加密的数据) 通过加密算法进行计算, 得到加密后的数据 * springsecurity的认证实现 * 1.根据用户名查询出来用户信息: 加密后的密码: tablePassword * 2.拿到前台前台传递过来的密码: 没有加密的密码 * 3.springseurity会拿到(salt+没有加密的密码),通过加密算法得到加密后的数据: operPassword * * 如果用户提交的密码 经过匹配, 与加密后的密码一致, 表示认证通过 * 如果用户提交的密码 经过匹配, 与加密后的密码不一致, 表示认证不通过 */ public class Demo { public static void main(String[] args){ BCryptPasswordEncoder be = new BCryptPasswordEncoder(); String p1 = be.encode("666"); System.out.println(p1); } }
-
**分析:**我们自定义的UserInfo和UserDetails怎么转换呢?
-
org.springframework.security.core.userdetails
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities();//指的角色数据 String getPassword(); String getUsername(); boolean isAccountNonExpired();// 帐户是否过期 boolean isAccountNonLocked();//帐户是否锁定 boolean isCredentialsNonExpired();//认证是否过期 boolean isEnabled();//帐户是否可用 }
-
实现类 org.springframework.security.core.userdetails.User
public class User implements UserDetails, CredentialsContainer { private static final long serialVersionUID = 500L; private String password; private final String username; private final Set<GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled;
-
所以自定义UserInfo转成 UserDetails,代码如下
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo userInfo = null;
try {
userInfo = userDao.findByUsername(username);
} catch (Exception e) {
e.printStackTrace();
}
List<SimpleGrantedAuthority> authorities = getAuthority(userInfo.getRoles());
User user = new User(userInfo.getUsername(),
"{noop}"+userInfo.getPassword(), //没有密码加密,对应的springsecurity配置文件不能配置加密的bean
userInfo.getStatus() == 0 ? false : true,
true,
true,
true,
authorities);
return user;
}
//作用就是返回一个List集合,集合中装入的是角色描述(这里没有根据用户名查询查询用户关联的角色,后期自己查询数据库)
public List<SimpleGrantedAuthority> getAuthority(List<Role> roles) {
List<SimpleGrantedAuthority> list = new ArrayList<>();
list.add(new SimpleGrantedAuthority("ROLE_USER"));
list.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
return list;
}
<!-- 切换成数据库中的用户名和密码 -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="userService">
<!-- 配置加密的方式 -->
<!--<security:password-encoder ref="passwordEncoder"/>-->
</security:authentication-provider>
</security:authentication-manager>
<!-- 配置加密类 -->
<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
-
基于数据库实现认证操作
- 步骤一: 配置pom.xm
- 步骤二: 定义web层, service层, dao层.
1.9 源码分析
问题:为什么DelegatingFilterProxy的filter-name必须是springSecurityFilterChain
DelegatingFilterProxy并不是真正的Filter,在其initFilterBean方法中会从WebApplicationContext根据delegate
来获取到
protected void initFilterBean() throws ServletException {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
if (this.targetBeanName == null) {
this.targetBeanName = this.getFilterName();
}
WebApplicationContext wac = this.findWebApplicationContext();
if (wac != null) {
this.delegate = this.initDelegate(wac);
}
}
}
}
在上这代码中this.targetBeanName=getFilterName()就是获取名称叫做springSecurityFilterChain
通过在doFilter就去中我们会发现真正干活的其实是delegate这个Filter,而delegate其实就是FilterChainProxy
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized(this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = this.findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = this.initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
this.invokeDelegate(delegateToUse, request, response, filterChain);
}
FilterChainProxy是spring在解析配置文件时装配到上下文中,并且beanName为springSecurityFilterChain,
因此在web.xml中需要配置filter-name为springSecurityFilterChain
spring-security.xml配置文件解析
<!-- 配置不拦截的资源 -->
<security:http pattern="/login.jsp" security="none"/>
<security:http pattern="/failer.jsp" security="none"/>
<security:http pattern="/css/**" security="none"/>
<security:http pattern="/img/**" security="none"/>
<security:http pattern="/plugins/**" security="none"/>
<security:http auto-config="true" use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN"/>
<security:form-login
login-page="/login.jsp"
login-processing-url="/login.do"
default-target-url="/index.jsp"
authentication-failure-url="/failer.jsp"
authentication-success-forward-url="/pages/main.jsp"
/>
</security:http>
http标签是自定义标签,我们可以在spring-security-config包中查看
http://www.springframework.org/schema/security=org.springframework.security.config.SecurityName
spaceHandler
继续查看SecurityNamespaceHandler类,在其init方法
public void init() {
loadParsers();
}
//在loadParsers()方法中,指定由HttpSecurityBeanDefinitionParser进行解析
parsers.put(Elements.HTTP, new HttpSecurityBeanDefinitionParser());
//在HttpSecurityBeanDefinitionParser完成具体解析的parse方法中
registerFilterChainProxyIfNecessary(pc, pc.extractSource(element));
//这里就是注册了名为springSecurityFilterChain的filterChainProxy类
//接下我们在看一下注册一系列Filter的地方createFilterChain,在这个方法中我们重点关注
AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element,
forceAutoConfig, pc, httpBldr.getSessionCreationPolicy(),
httpBldr.getRequestCache(), authenticationManager,
httpBldr.getSessionStrategy(), portMapper, portResolver,
httpBldr.getCsrfLogoutHandler());
我们可以查看AuthenticationConfigBuilder创建代码
public AuthenticationConfigBuilder(Element element, boolean forceAutoConfig,
ParserContext pc, SessionCreationPolicy sessionPolicy,
BeanReference requestCache, BeanReference authenticationManager,
BeanReference sessionStrategy, BeanReference portMapper,
BeanReference portResolver, BeanMetadataElement csrfLogoutHandler) {
this.httpElt = element;
this.pc = pc;
this.requestCache = requestCache;
autoConfig = forceAutoConfig
| "true".equals(element.getAttribute(ATT_AUTO_CONFIG));
this.allowSessionCreation = sessionPolicy != SessionCreationPolicy.NEVER
&& sessionPolicy != SessionCreationPolicy.STATELESS;
this.portMapper = portMapper;
this.portResolver = portResolver;
this.csrfLogoutHandler = csrfLogoutHandler;
createAnonymousFilter();
createRememberMeFilter(authenticationManager);
createBasicFilter(authenticationManager);
createFormLoginFilter(sessionStrategy, authenticationManager);
createOpenIDLoginFilter(sessionStrategy, authenticationManager);
createX509Filter(authenticationManager);
createJeeFilter(authenticationManager);
createLogoutFilter();
createLoginPageFilterIfNeeded();
createUserDetailsServiceFactory();
createExceptionTranslationFilter();
}
分析如下:
2.0 服务器端方法级权限控制
在服务器端我们可以通过Spring security提供的注解对方法来进行权限控制。Spring Security在方法的权限控制上
支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解,这三种注解默认都是没有启用的,需要
单独通过global-method-security元素的对应属性进行启用
基于方法权限控制,需要开启注解,常用有两种方式:
-
方式一; 通过配置文件, 开启注解的使用
-
<security:global-method-security jsr250-annotations=“enabled”/>
jsr250注解.需要额外引入依赖
-
<security:global-method-security secured-annotations=“enabled”/>
springSecurity自身的,不需额外引入依赖
-
-
方式二:注解开启
@EnableGlobalMethodSecurity :Spring Security默认是禁用注解的,要想开启注解,需要在继承
WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,并在该类中将
AuthenticationManager定义为Bean。
2.0.1 JSR-250注解
-
概述
在spring-security.xml文件中启用JSR250注解支持。
JSR概念 翻译自: Wikipedia 维基百科全书
JSR 250是Java规范请求以发展为目标注解(即,关于不属于程序本身的软件程序的信息)JavaSE和JavaEE适用于各种不同技术的平台。据设想,各种JSR将使用注释来启用陈述式编程风格。在JavaEE组件JSR中保持一致性特别有价值,但允许JavaEE和JavaSE之间的一致性也是很有价值的。
-
具体实现
步骤一: 在pom.xml导入依赖
<!-- https://mvnrepository.com/artifact/javax.annotation/jsr250-api --> <dependency> <groupId>javax.annotation</groupId> <artifactId>jsr250-api</artifactId> <version>1.0</version> </dependency>
步骤二: 在spring-security配置文件上, 开启注解支持
<security:global-method-security jsr250-annotations="enabled"/>
步骤三: 在测试上面使用注解
@RolesAllowed表示访问对应方法时所应该具有的角色
//查询全部产品 @RequestMapping("/findAll.do") @RolesAllowed("admin")//也可以ROLE_ADMIN public ModelAndView findAll() throws Exception { ModelAndView mv = new ModelAndView(); List<Product> ps = productService.findAll(); mv.addObject("productList", ps); mv.setViewName("product-list1"); return mv; }
示例:
@RolesAllowed({“USER”, “ADMIN”}) 该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省
略前缀ROLE_,实际的权限可能是ROLE_ADMIN@PermitAll表示允许所有的角色进行访问,也就是说不进行权限控制
@DenyAll是和PermitAll相反的,表示无论什么角色都不能访问
2.0.2 springSecurity自身注解,基于方法进行授权
-
步骤一: 在springsecurity.xml配置
<security:global-method-security secured-annotations=“enabled”/
@Secured注解标注的方法进行权限控制的支持,其值默认为disabled。
-
步骤二: @Secured注解,在controller里面的方法上面使用
注意: @Secured注解里面的角色名称必须带ROLE
2.0.3 其它注解
@PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问
示例:
@PreAuthorize(“#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)”)
void changePassword(@P(“userId”) long userId ){ }
这里表示在changePassword方法执行之前,判断方法参数userId的值是否等于principal中保存的当前用户的
userId,或者当前用户是否具有ROLE_ADMIN权限,两种符合其一,就可以访问该方法。
@PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
>示例:
>@PostAuthorize
>User getUser("returnObject.userId == authentication.principal.userId or
>hasPermission(returnObject, 'ADMIN')");
>
>
2.1 页面端标签控制权限(了解)
在jsp页面中我们可以使用spring security提供的权限标签来进行权限控制
-
步骤一: pom.xml导入坐标依赖
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>version</version> </dependency>
-
步骤二: jsp页面使用
<%@taglib uri=“http://www.springframework.org/security/tags” prefix=“security”%>
-
步骤三:使用标签
在jsp中我们可以使用以下三种标签,其中authentication代表的是当前认证对象,可以获取当前认证对象信息,例
如用户名。其它两个标签我们可以用于权限控制
authentication :显示登录的用户名
<security:authentication property="" htmlEscape="" scope="" var=""/>
- property: 只允许指定Authentication所拥有的属性,可以进行属性的级联获取,如“principle.username”,
不允许直接通过方法进行调用 - htmlEscape:表示是否需要将html进行转义。默认为true。
- scope:与var属性一起使用,用于指定存放获取的结果的属性名的作用范围,默认我pageContext。Jsp中拥
有的作用范围都进行进行指定 - var: 用于指定一个属性名,这样当获取到了authentication的相关信息后会将其以var指定的属性名进行存
放,默认是存放在pageConext中
authorize : authorize是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示
<security:authorize access="" method="" url="" var=""></security:authorize>
- access: 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限
- method:method属性是配合url属性一起使用的,表示用户应当具有指定url指定method访问的权限,
- method的默认值为GET,可选值为http请求的7种方法
- url:url表示如果用户拥有访问指定url的权限即表示可以显示authorize标签包含的内容
- var:用于指定将权限鉴定的结果存放在pageContext的哪个属性中
限控制
-
步骤一: pom.xml导入坐标依赖
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-taglibs</artifactId> <version>version</version> </dependency>
-
步骤二: jsp页面使用
<%@taglib uri=“http://www.springframework.org/security/tags” prefix=“security”%>
-
步骤三:使用标签
在jsp中我们可以使用以下三种标签,其中authentication代表的是当前认证对象,可以获取当前认证对象信息,例
如用户名。其它两个标签我们可以用于权限控制
authentication :显示登录的用户名
<security:authentication property="" htmlEscape="" scope="" var=""/>
- property: 只允许指定Authentication所拥有的属性,可以进行属性的级联获取,如“principle.username”,
不允许直接通过方法进行调用 - htmlEscape:表示是否需要将html进行转义。默认为true。
- scope:与var属性一起使用,用于指定存放获取的结果的属性名的作用范围,默认我pageContext。Jsp中拥
有的作用范围都进行进行指定 - var: 用于指定一个属性名,这样当获取到了authentication的相关信息后会将其以var指定的属性名进行存
放,默认是存放在pageConext中
authorize : authorize是用来判断普通权限的,通过判断用户是否具有对应的权限而控制其所包含内容的显示
<security:authorize access="" method="" url="" var=""></security:authorize>
- access: 需要使用表达式来判断权限,当表达式的返回结果为true时表示拥有对应的权限
- method:method属性是配合url属性一起使用的,表示用户应当具有指定url指定method访问的权限,
- method的默认值为GET,可选值为http请求的7种方法
- url:url表示如果用户拥有访问指定url的权限即表示可以显示authorize标签包含的内容
- var:用于指定将权限鉴定的结果存放在pageContext的哪个属性中