spring security基础

介绍

技术 特性 适用场景 说明
Spring Security - 认证
- 授权
- 安全防护
- 和spring mvc容易结合
- 加密功能
- 会话管理
- 缓存支持
- remember me
企业级安全 基于Spring开发的强大的、易于定制的实现认证与授权的框架
- 官网

基础概念

概念 说明 应用场景
决策管理器 Spring提供了3个决策管理器:
- AffirmativeBased:一票通过,只要有一个投票器通过就允许访问
- ConsensusBased:有一半以上投票器通过才允许访问资源
- UnanimousBased:所有投票器都通过才允许访问
投票器 -

接入方式

Spring Security的接入方式一共有两种:

  • 基于注解方式
  • 基于xml配置方式

基于注解方式

Spring Security的Java 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.security.code;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import javax.sql.DataSource;

*/
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{

public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

/**
* 配置user-detail服务
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{

}

/**
* 拦截请求
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http)throws Exception{

}

/**
* 拦截请求
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {

}
}

@EnableWebSecurity 注解将会启用Web安全功能。
WebSecurityConfigurerAdapter共有三个configure方法:

方法 说明
configure(WebSecurity) 通过重载,配置Spring Security的Filter链
configure(HttpSecurity) 通过重载,配置如何通过拦截器保护请求
configure(AuthenticationManagerBuilder) 通过重载,配置user-detail服务

初始化springSecurityFilter注册类

1
2
3
4
5
6
7
8
9
10
package com.security.code;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

/**
* 注册springSecurityFilter
*/
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer
{
}

这里可以没有任何实现。至此,基于注解方式的接入就完成了。

基于XML配置方式

配置web.xml

1
2
3
4
5
6
7
8
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

增加spring-security.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
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 auto-config="true" use-expressions="true">
...
</security:http>

<security:authentication-manager>
..
</security:authentication-manager>

</beans>

将spring-security.xml并引入到application.xml内。

用户认证存储方式

用户认证存储方式

用户认证存储方式 适用场景 说明
内存 开发、测试 将用户名、密码、权限等数据存储在内存中
数据库 通常 将用户数据存储在关系型数据库中,并通过jdbc进行访问
LDAP 查询多、修改少 轻量目录访问协议存储
自定义 生产环境 这种方式更为灵活,更适合在生产环境使用。
这种方式不再局限于存储环境。
自定义的方式也很简单,只需要提供一个UserDetailService接口实现即可。

使用基于内存的用户存储

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 配置user-detail服务
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{
//基于内存的用户存储、认证
auth.inMemoryAuthentication()
.withUser("admin").password("admin").roles("ADMIN","USER")
.and()
.withUser("user").password("user").roles("USER");
}

代码里通过方法auth.inMemoryAuthentication()获取AuthenticationManagerBuilder对象,并设置了两个用户admin和user同时设置对应密码和所拥有的权限。

这里需要注意的是,roles()方法是authorities()方法的简写形式。roles()方法所给定的值都会加一个”ROLE_”前缀,并将其作为权限授予用户。实际上,如下用户配置与上面程序是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 配置user-detail服务
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{
//基于内存的用户存储、认证
auth.inMemoryAuthentication()
.withUser("admin").password("admin").authorities("ROLE_AMIN","ROLE_USER")
.and()
.withUser("user").password("user").authorities("ROLE_USER");

}

除了上面的方法还有一些其它方法可以配置用户的详细信息:

方法 说明
accountExpired(boolean) 定义账号是否已经过期
accountLocked(boolean) 定义账号是否已经锁定
and() 用来连接配置
authorities(GrantedAuthority...) 授予某个用户一项或多项权限
authorities(List<? extends GrantedAuthority>) 授予某个用户一项或多项权限
authorities(String...) 授予某个用户一项或多项权限
credentialsExpired(boolean) 定义凭证是否已经过期
disabled(boolean) 定义账号是否已被禁用
password(String) 定义用户的密码
roles(String...) 授予某个用户一项或多项角色

xml配置方式

1
2
3
4
5
6
7
8
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="admin" authorities="ROLE_ADMIN" password="admin"/>
<security:user name="user" authorities="ROLE_USER" password="user"/>
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>

基于数据库表用户存储认证

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 配置user-detail服务
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{
//基于数据库的用户存储、认证
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select account,password,true from user where account=?")
.authoritiesByUsernameQuery("select account,role from user where account=?");
}

第一个查询语句获取了用户的基本信息,第二个查询语句获取了用户权限数据。

xml配置方式

1
2
3
4
5
6
7
8
<security:authentication-manager>
<security:authentication-provider>
<security:jdbc-user-service data-source-ref="dataSource"
authorities-by-username-query="select account,role from user where account=?"
users-by-username-query="select account,password,true from user where account=?"/>
<security:password-encoder ref="bcryptEncoder"/>
</security:authentication-provider>
</security:authentication-manager>

自定义的用户存储认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.blog.admin.security;

import com.blog.admin.dao.UserDao;
import db.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomSecurityUserDetailsService implements UserDetailsService{

@Autowired
private UserDao userDao;

/**
* 校验用户
* @param username
* @return
* @throws UsernameNotFoundException
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

User user = userDao.queryByUserName(username);
if(user == null)throw new UsernameNotFoundException("user not found");

return new org.springframework.security.core.userdetails.User(user.getAccount(),user.getPassword(),userDao.getUserGrantedAuthoritys(user.getId()));
}
}

自定义的方式只要实现接口方法loadUserByUsername(String username)即可,返回代表用户的UserDetails对象。调用方式也很简单:

注解编码方式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 自定义用户存储
*
* @author itguang
* @create 2017-12-29 8:20
**/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new CustomSecurityUserDetailsService());
}
}

基于xml方式调用

1
2
3
4
<security:authentication-manager>
<security:authentication-provider user-service-ref="CustomSecurityUserDetailsService">
</security:authentication-provider>
</security:authentication-manager>

密码加密策略

密码加密策略 说明
NoOpPasswordEncoder 明文方式保存
BCtPasswordEncoder 强hash方式加密
StandardPasswordEncoder SHA-256方式加密
实现PasswordEncoder接口 自定义加密方式

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 配置user-detail服务
* @param auth
* @throws Exception
*/
@Override
public void configure(AuthenticationManagerBuilder auth)throws Exception{
//基于数据库的用户存储、认证
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select account,password,true from user where account=?")
.authoritiesByUsernameQuery("select account,role from user where account=?")
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}

通过方法passwordEncoder传入对应的加密实例即可。

xml配置方式

1
2
3
4
5
6
7
8
9
10
<security:authentication-manager>
<security:authentication-provider>
<security:jdbc-user-service data-source-ref="dataSource"
authorities-by-username-query="select account,role from user where account=?"
users-by-username-query="select account,password,true from user where account=?"/>
<security:password-encoder ref="bcryptEncoder"/>
</security:authentication-provider>
</security:authentication-manager>

<bean id="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

请求拦截策略

spring security的请求拦截匹配有两种风格,ant风格和正则表达式风格。
编码方式是通过重载configure(HttpSecurity)方法实现。

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 拦截请求
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http)throws Exception{
http.authorizeRequests()
.antMatchers("/","/css/**","/js/**").permitAll() //任何人都可以访问
.antMatchers("/admin/**").access("hasRole('ADMIN')") //持有ADMIN权限的用户可以访问
.antMatchers("/user/**").hasAuthority("ROLE_USER"); //持有USER权限的用户可以访问
}

上面使用的是ant风格的匹配,可以通过http.regexMatcher()方法使用正则表达式风格。

xml配置方式

1
2
3
4
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/admin/*" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/user/*" access="hasRole('ROLE_USER')"/>
</security:http>

除了上面的请求保护方法外,还有一些方法能够用来定义如何保护请求。

方法 说明
access(String) 如果给定的SpEL表达式计算结果为true,就允许访问
anonymous() 允许匿名用户访问
authenticated() 允许认证过的用户访问
denyAll() 无条件拒绝所有访问
fullyAuthenticated() 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问
hasAnyAuthority(String...) 如果用户具备给定权限中的某一个的话,就允许访问
hasAnyRole(String...) 如果用户具备给定角色中的某一个的话,就允许访问
hasAuthority(String) 如果用户具备给定权限的话,就允许访问
hasIpAddress(String) 如果请求来自给定IP地址的话,就允许访问
hasRole(String) 如果用户具备给定角色的话,就允许访问
not() 对其他访问方法的结果求反
permitAll() 无条件允许访问
rememberMe() 如果用户是通过Remember-me功能认证的,就允许访问

防止CSRF

spring security从版本3.2开始,默认就会启用CSRF防护。
spring security通过一个同步token的方式来实现CSRF防护功能。它会拦截状态变化的请求,并检查CSRF token。如果请求中不包含CSRF token的话,或者token不能与服务器端的token匹配,请求就会失败,并抛出CsrfException异常。

如果使用JSP作为页面模板的话,需要作的非常简单。

1
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

这样就spring security就会自动生成csrf token。
如果想关闭csrf防护,需要作的也很简单,只需要调用一下csrf().disable();即可。

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 拦截请求
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http)throws Exception{
http.authorizeRequests()
.antMatchers("/","/css/**","/js/**").permitAll() //任何人都可以访问
.antMatchers("/admin/**").access("hasRole('ADMIN')") //持有ADMIN权限的用户可以访问
.antMatchers("/user/**").hasAuthority("ROLE_USER")
.and().csrf().disable();
}

xml配置方式

1
2
3
4
5
<security:http auto-config="true" use-expressions="true">
<security:csrf disabled="true" />
<security:intercept-url pattern="/admin/*" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/user/*" access="hasRole('ROLE_USER')" />
</security:http>

remember-me功能

remember-me是一个很重要的功能,用户肯定不希望每次都输入用户名密码进行登录。
spring security提供的remember-me功能使用起来非常简单。
启用这个功能只需要调用rememberMe()方法即可。

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 拦截请求
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http)throws Exception{
http.authorizeRequests()
.antMatchers("/","/css/**","/js/**").permitAll() //任何人都可以访问
.antMatchers("/admin/**").access("hasRole('ADMIN')") //持有user权限的用户可以访问
.antMatchers("/user/**").hasAuthority("ROLE_USER")
.and().rememberMe().key("abc").rememberMeParameter("remember_me").rememberMeCookieName("my-remember-me").tokenValiditySeconds(86400);
}

xml配置方式

1
2
3
4
5
<security:http auto-config="true" use-expressions="true">
<security:remember-me />
<security:intercept-url pattern="/admin/*" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/user/*" access="hasRole('ROLE_USER')" />
</security:http>

remember-me有多种参数可以选择配置。 这里有个需要注意的地方 ,在登录页面内的remember-me的input标签内不要设置value的值,否则remember-me功能将不会生效。

自定义登录页面

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 拦截请求
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http)throws Exception{
http.authorizeRequests()
.antMatchers("/","/css/**","/js/**").permitAll() //任何人都可以访问
.antMatchers("/admin/**").access("hasRole('ADMIN')") //持有user权限的用户可以访问
.antMatchers("/user/**").hasAuthority("ROLE_USER")
.and().formLogin()
.loginPage("/login").usernameParameter("username").passwordParameter("password")
.and().exceptionHandling().accessDeniedPage("/loginfail");
}

通过formLogin()方法来设置使用自定义登录页面,loginPage是登录页面地址,accessDeniePage登录失败跳转地址。

xml配置方式

1
2
3
4
5
6
7
8
9
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/admin/*" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/user/*" access="hasRole('ROLE_USER')" />
<security:form-login login-page="/login"
username-parameter="username"
password-parameter="password"
authentication-failure-url="/loginfail"
default-target-url="/"/>
</security:http>

用户信息缓存

Spring Security内置的缓存是基于ehcache的。

过滤器

basic认证

匿名登录

管理会话

  • 默认情况下,后登陆的用户会把先登录的用户踢出系统。
  • 后面的用户禁止登录

单点登录

Spring Security没有实现自己的SSO,而是整合了耶鲁大学单点登陆(JA-SIG),这 是当前使用很广泛的一种SSO实现,它是基于中央认证服务CAS(Center Authentication Service)的结构实现的。
CAS官网

信道安全

方法 说明
requiresInsecure() 始终使用http
requiresSecure() 强制使用https

注解编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 拦截请求
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http)throws Exception{
http.authorizeRequests()
.antMatchers("/","/css/**","/js/**").permitAll() //任何人都可以访问
.antMatchers("/admin/**").access("hasRole('ADMIN')") //持有ADMIN权限的用户可以访问
.antMatchers("/user/**").hasAuthority("ROLE_USER")
.and()
.requiresChannel().antMatchers("/admin/info").requiresSecure();
}

不论何时,只要是对“/admin/info”的请求,spring security都认为需要安全性通道,并自动将请求重定向到https上。

xml配置方式

1
2
3
4
5
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/admin/*" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/user/*" access="hasRole('ROLE_USER')" />
<security:intercept-url pattern="/admin/info" access="hasRole('ROLE_ADMIN')" requires-channel="https"/>
</security:http>

配置digest验证

UserDetails接口方法

方法 说明
GrantedAuthority[] getAuthorities() 用户拥有的权限
String getPassword() 密码
String getUsername() 用户名
boolean isAccountNonExpired() 用户账号是否过期
boolean isAccountNonLocked() 用户账号是否被锁定
boolean isCredentialsNonExpired() 用户密码是否过期
boolean isEnabled() 用户是否可用

UserDetailsService接口方法

方法 说明
UserDetails loadUserByUsername(String username) 根据用户名获得登录用户的信息

自定义登陆

自定义登陆成功与失败处理逻辑

实现类 说明
AuthenticationSuccessHandler 自定义登陆成功接口
AuthenticationFailureHandler 自定义登陆失败接口

获取登陆之后的用户名

1
2


源码

https://github.com/iamwlb/spring-security-basic

附录

需要自己实现的重要类

说明
UserDetailsService 读取登录用户信息、权限
AbstractSecurityInterceptor 这个类是用来继承的,还要实现servler的Filter,过滤url
FilterInvocationSecurityMetadataSource 读取url资源
AccessDecisionManager 控制访问权限

参考

坚持原创技术分享,您的支持将鼓励我继续创作!
0%