spring security使用学习
公司一直用的spring security的安全验证框架,是时候深入的研究下。
首先理解下安全:
安全包括两个主要操作:
第一个被称为“认证”,是为用户建立一个他所声明的主体。主体一般是指用户,设备或可以在系统中执行动作的其他系统。
第二个叫“授权”,指的是一个用户能否在应用中执行某个操作,在到达授权判断之前,身份的主体已经由身份验证过程建立。
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Web请求级别的保护
对于请求级别的安全性来说,主要时通过保护一个或多个URL,使得只有特定的用户才能访问,并其他用户访问该URL的内容
jar包:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${springspring-security-web.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${springspring-security-web.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
---------------------------------------------------------------------
web.xml过滤器链配置 :
<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>
这里配置了一个servlet的filter,这个filter本身并不处理具体的请求,它其实是一个filter chain,它内部包含了一个由多个spring security提供的filter的list,它负责把请求委派给list中的每一个filter进行处理。
UsernamePasswordAuthenticationFilter:该filter用于用户初次登录时验证用户身份(authentication)。该filter只在初次认证时存在,一旦认证通过将会从 filter chain中移除。
FilterSecurityInterceptor:当用户登入成功之后,每次发送请求都会使用该filter检查用户是否已经通过了认证。如果通过了认证,就放行,否则转向登录页面。
两个filter的差别在于: 第一个负责初次登入时的用户检查,这个检查需要根据用户提供的用户名和密码去数据库核对,若存在,将相关信息封装在一个Authentication对象中。这个filter可以说是处理初次登录时的authentication工作。而第二个filter则不需要像每个filter每次都去查询数据库,它只需要从 security context中查看当前请求用户对应的Authentication 对象是否已经存在就可以了,这个filter处理的是登入成功之后的authentication工作。这个filter是需要拦截每次请求的。
接下来重点说一下他的配置文件:这里有更详细的
http://www.blogjava.net/hhhaaawwwkkk/archive/2011/12/13/366222.html
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="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-4.1.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
<!--不用验证的东东--!>
<http pattern="/login.html*" security="none"></http>
<http pattern="/css/**" security="none"></http>
<http pattern="/images/**" security="none"></http>
<http pattern="/img/**" security="none"></http>
<http pattern="/js/**" security="none"></http>
<http pattern="/assets/**" security="none"></http>
<http pattern="/commons/**" security="none"></http>
<http pattern="/share/**" security="none" />
<!--即与方法的权限设置,关于global-method-security说明戳http://blog.csdn.net/w410589502/article/details/51508306--!>
<global-method-security pre-post-annotations="enabled" />
<!-- 认证配置,使用userDetailsManager提供用户的信息 -->
<authentication-manager alias="myAuthenticationManager">
<authentication-provider user-service-ref="userDetailsManager">
<!-- 加密密码 -->
<password-encoder hash="md5" />
</authentication-provider>
</authentication-manager>
<!-- 用户详细信息管理:数据源、用户缓存(通过数据库管理用户、角色、权限、资源) -->
<beans:bean id="userDetailsManager"
class="com.ch.housekeeping.base.security.XaUserDetailsService">
</beans:bean>
<!-- 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源。 -->
<beans:bean id="myAccessDecisionManager"
class="com.ch.housekeeping.base.security.XaAccessDecisionManagerService" />
<!-- 资源源数据定义,将所有的资源和权限对应关系建立起来,即定义某一资源可以被哪些角色去访问。 -->
<beans:bean id="mySecurityMetadataSource" init-method="loadResourceDefine"
class="com.ch.housekeeping.base.security.XaSecurityMetadataSourceService">
</beans:bean>
<!-- 1.URL过滤器或方法拦截器:用来拦截URL或者方法资源对其进行验证,其抽象基类为AbstractSecurityInterceptor
2.资源权限获取器:用来取得访问某个URL或者方法所需要的权限,接口为SecurityMetadataSource 3.访问决策器:用来决定用户是否拥有访问权限的关键类,其接口为AccessDecisionManager
调用顺序为:AbstractSecurityInterceptor调用SecurityMetadataSource取得资源的所有可访问权限, 然后再调用AccessDecisionManager来实现决策,确定用户是否有权限访问该资源。 -->
<!-- 自定义的filter, 必须包含authenticationManager, accessDecisionManager, securityMetadataSource三个属性 -->
<beans:bean id="mySecurityFilter"
class="com.ch.housekeeping.base.security.XaFilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="myAuthenticationManager" />
<beans:property name="accessDecisionManager" ref="myAccessDecisionManager" />
<beans:property name="securityMetadataSource" ref="mySecurityMetadataSource" />
</beans:bean>
<!-- HTTP安全配置 -->
<!--详细说下标签的含义:
auto-config="true":会自动提供以下3个认证相关的功能
1、HTTP基本认证
2、Form登录认证
3、退出
你也可以使用配置元素来实现这三个功能,比使用auto-config提供的功能更精确;
access-denied-page:
访问拒绝时转向的页面
form-login介绍:
http元素下的form-login元素是用来定义表单登录信息的。当我们什么属性都不指定的时候Spring Security会为我们生成一个默认的登录页面。如果不想使用默认的登录页面,我们可以指定自己的登录页面
自定义登录页面时的属性:
Ø username-parameter:表示登录时用户名使用的是哪个参数,默认是“j_username”。
Ø password-parameter:表示登录时密码使用的是哪个参数,默认是“j_password”。
Ø login-processing-url:表示登录时提交的地址,默认是“/j-spring-security-check”。这个只是Spring Security用来标记登录页面使用的提交地址,真正关于登录这个请求是不需要用户自己处理的。
--!>
<http auto-config="true" access-denied-page="/commons/403.html" use-expressions="true">
<!--这里是对登录页面的放行,和上面的指定一个http元素的安全性为none一样--!>
<intercept-url pattern="/login.html*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<!--default-target-url 验证成功后跳转的页面 ,
authentication-failure-url 验证失败后的页面--!>
<form-login login-page="/login.html" default-target-url="/index.html"
authentication-failure-url="/login.html?login_error=true" />
<!--logout 标签 退出功能
invalidate-session是否销毁Session
logout-url logout地址
logout-success-url logout成功后要跳转的地址
使得HTTP session失效(如果invalidate-session属性被设置为true);
清除SecurityContex(真正使得用户退出);
将页面重定向至logout-success-url指明的URL。--!>
<logout invalidate-session="true" logout-success-url="/login.html"
logout-url="/j_spring_security_logout" />
<!-- 增加一个自定义的filter, 放在FILTER_SECURITY_INTERCEPTOR之前, 实现用户, 角色, 权限, 资源的数据库管理 -->
<custom-filter ref="mySecurityFilter" after="FILTER_SECURITY_INTERCEPTOR" />
</http>
</beans:beans>
ok了解下他的原理:
spring security是不能修改fitler的所以通过插入fitler来实现的。
首先登陆验证拦截器AuthenticationProcessingFilter;还有就是对访问的资源管理,所以资源管理拦截器AbstractSecurityInterceptor要讲;但拦截器里面的实现需要一些组件来实现,所以就有了AuthenticationManager、accessDecisionManager等组件来支撑。
现在先大概过一遍整个流程,用户登陆,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而且AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等),如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等),如果权限足够,则返回,权限不够则报错并调用权限不足页面。
简单点理解下:
1.当你服务器跑起来的时候,通过过滤器就会加载你数据库的每一个角色的所有访问权限的资源信息,然后放到一个角色为key,value为权限list的map中。FilterInvocationSecurityMetadataSource处理的
2.然后这时候你登陆时,会通过拦截查找有没有这个用户,和这个用户具有的权限,UserDetailsService这里是通过这个封装到UserDetails(包括用户名。密码,权限list),然后访问url的时候将url的权限和用户具有的权限比对。AccessDecisionManager
代码:
用户详细信息管理:数据源、用户缓存(通过数据库管理用户、角色、权限、资源)
com.ch.housekeeping.base.security.XaUserDetailsService
@Service("XaUserDetailsService")
@Transactional(readOnly = true)
public class XaUserDetailsService implements UserDetailsService {
protected static final String ROLE_PREFIX = "ROLE_";
protected static final GrantedAuthority DEFAULT_USER_ROLE = new SimpleGrantedAuthority(
ROLE_PREFIX + "USER");
@Autowired
private XaCmsUserRepository xaCmsUserRepository;
@Autowired
private XaCmsResourceRepository xaCmsResourceRepository;
<!--这里加载用户的权限信息封装到UserDetails放到全局SecurityContextHolder中-->
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
XaUserDetails xaUserDetails = new XaUserDetails();
try {
XaCmsUser user = xaCmsUserRepository.findByUserName(username,
XaConstant.UserStatus.status_normal);
List<String> rList = xaCmsResourceRepository
.findRoleNameByUserName(username);
if(XaUtil.isEmpty(user)){
return xaUserDetails;//("登录失败:未找到用户信息");
}
xaUserDetails.setUsername(user.getUserName());
xaUserDetails.setPassword(user.getPassword());
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
for (String roleName : rList) {
GrantedAuthority authority = new SimpleGrantedAuthority(
roleName);
authorities.add(authority);
}
xaUserDetails.setAuthorities(authorities);
} catch (Exception e) {
e.printStackTrace();
}
return xaUserDetails;
}
}
//这里是UserDetails
public class XaUserDetails implements UserDetails {
/**
*
*/
private static final long serialVersionUID = 198796435498464L;
private List<GrantedAuthority> authorities;
private String password;
private String username;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
/**
* 未处理字段-默认值
* */
public XaUserDetails() {
accountNonExpired = true;
accountNonLocked = true;
credentialsNonExpired = true;
enabled = true;
}
public List<GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<GrantedAuthority> authorities) {
this.authorities = authorities;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public boolean isAccountNonExpired() {
return accountNonExpired;
}
public void setAccountNonExpired(boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public boolean isAccountNonLocked() {
return accountNonLocked;
}
public void setAccountNonLocked(boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
}
资源源数据定义
com.ch.housekeeping.base.security.XaSecurityMetadataSourceService
//这里是加载所有url和权限(或角色)的对应关系
@Service(“XaSecurityMetadataSourceService”)
@Transactional(readOnly = true)
public class XaSecurityMetadataSourceService implements
FilterInvocationSecurityMetadataSource {
@Autowired
private XaCmsResourceRepository xaCmsResourceRepository;
@Autowired
private XaCmsRoleRepository xaCmsRoleRepository;
private static Map<String, Collection<ConfigAttribute>> roleMap = null;
public void loadResourceDefine() {
if (roleMap == null) {
roleMap = new LinkedHashMap<String, Collection<ConfigAttribute>>();
}
Iterator<XaCmsRole> iterator = xaCmsRoleRepository.findAllXaCmsRole(XaConstant.RoleStatus.status_normal).iterator();
while (iterator.hasNext()) {
XaCmsRole mcr = iterator.next();
List<String> urlList = xaCmsResourceRepository
.findResourceByRoleId(mcr.getRoleId());
for (String url : urlList) {
if (XaUtil.isNotEmpty(url)) {
if (XaUtil.isNotEmpty(mcr.getRoleName())) {
ConfigAttribute configAttribute = new SecurityConfig(
mcr.getRoleName());
Collection<ConfigAttribute> configAttributes = null;
if(url.length()>0){
if(!"/".equals(url.substring(0, 1))){
url="/"+url;
}
}
if (roleMap.containsKey(url)) {
configAttributes = roleMap.get(url);
} else {
configAttributes = new ArrayList<ConfigAttribute>();
}
configAttributes.add(configAttribute);
roleMap.put(url, configAttributes);
}
}
}
}
}
//参数是要访问的url,返回这个url对于的所有权限(或角色)
public Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException {
String url = ((FilterInvocation) object).getRequestUrl();
int firstQuestionMarkIndex = url.indexOf("?");
if (firstQuestionMarkIndex != -1) {
url = url.substring(0, firstQuestionMarkIndex);
}
if (roleMap == null) {
loadResourceDefine();
}
return roleMap.get(url);
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
// TODO Auto-generated method stub
return null;
}
public boolean supports(Class<?> clazz) {
// TODO Auto-generated method stub
return true;
}
//将map设置为空,重新加载
public static void reset(){
roleMap = null;
}
}
//拦截器AbstractSecurityInterceptor
//每次访问资源都会被这个拦截器拦截,会执行doFilter这个方法,这个方法调用了invoke方法,其中fi断点显示是一个url。beforeInvocation这个方法,它首先会调用MyInvocationSecurityMetadataSource类的getAttributes方法获取被拦截url所需的权限,在调用MyAccessDecisionManager类decide方法判断用户是否够权限。弄完这一切就会执行下一个拦截器
public class XaFilterSecurityInterceptor extends AbstractSecurityInterceptor
implements Filter {
//注入
private SecurityMetadataSource securityMetadataSource;
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return securityMetadataSource;
}
public SecurityMetadataSource getSecurityMetadataSource() {
return securityMetadataSource;
}
public void setSecurityMetadataSource(
SecurityMetadataSource securityMetadataSource) {
this.securityMetadataSource = securityMetadataSource;
}
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}
//登陆后,每次访问资源都通过这个拦截器拦截
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSourcegetAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
public void invoke(FilterInvocation fi) throws IOException,
ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
public void destroy() {
// TODO Auto-generated method stub
}
}
//访问决策器AccessDecisionManager
//decide方法里面写的就是授权策略,比对url的权限和用户具有的权限
@Service(“XaAccessDecisionManagerService”)
@Transactional(readOnly = true)
public class XaAccessDecisionManagerService implements AccessDecisionManager {
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if (configAttributes == null) {
return;
}
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
String needPermission = configAttribute.getAttribute();
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (needPermission.equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("没有权限访问");
}
public boolean supports(ConfigAttribute attribute) {
return true;
}
public boolean supports(Class<?> clazz) {
return true;
}
}
这样整体流程在分析:
1、当Web服务器启动时,通过Web.xml中对于Spring Security的配置,加载过滤器链,那么在加载MyFilterSecurityInterceptor类时,
会注入MyInvocationSecurityMetadataSourceService、MyUserDetailsService、MyAccessDecisionManager类。
2、该MyInvocationSecurityMetadataSourceService类在执行时会提取数据库中所有的用户权限,形成权限列表;
并循环该权限列表,通过每个权限再从数据库中提取出该权限所对应的资源列表,并将资源(URL)作为key,权限列表作为value,形成Map结构的数据。
3、当用户登录时,AuthenticationManager进行响应,通过用户输入的用户名和密码,然后再根据用户定义的密码算法和盐值等进行计算并和数据库比对,
当正确时通过验证。此时MyUserDetailsService进行响应,根据用户名从数据库中提取该用户的权限列表,组合成UserDetails供Spring Security使用。
4、当用户点击某个功能时,触发MyAccessDecisionManager类,该类通过decide方法对用户的资源访问进行拦截。
用户点击某个功能时,实际上是请求某个URL或Action, 无论.jsp也好,.action或.do也好,在请求时无一例外的表现为URL。
还记得第2步时那个Map结构的数据吗? 若用户点击了”login.action”这个URL之后,那么这个URL就跟那个Map结构的数据中的key对比,若两者相同,
则根据该url提取出Map结构的数据中的value来,这说明:若要请求这个URL,必须具有跟这个URL相对应的权限值。这个权限有可能是一个单独的权限,
也有可能是一个权限列表,也就是说,一个URL有可能被多种权限访问。
那好,我们在MyAccessDecisionManager类的decide这个方法里,将通过URL取得的权限列表进行循环,然后跟第3步中登录的用户所具有的权限进行比对,
若相同,则表明该用户具有访问该资源的权利。 不大明白吧? 简单地说, 在数据库中我们定义了访问“LOGIN”这个URL必须是具有ROLE_ADMIN权限的人来访问,那么,登录用户恰恰具有该ROLE_ADMIN权限,两者的比对过程中,就能够返回TRUE,可以允许该用户进行访问。就这么简单!
不过在第2步的时候,一定要注意MyInvocationSecurityMetadataSoruceService类的loadResourceDefine()方法中,形成以URL为key,权限列表为value的Map时,要注意key和Value的对应性,避免Value的不正确对应形成重复,这样会导致没有权限的人也能访问到不该访问到的资源。还有getAttributes()方法,要有 url.indexOf(“?”)这样的判断,要通过判断对URL特别是Action问号之前的部分进行匹配,防止用户请求的带参数的URL与你数据库中定义的URL不匹配,造成访问拒绝!然后角色,资源,用户,角色用户,角色资源实体这里就不给出来了
参考资料:
http://blog.csdn.net/benjamin_whx/article/details/39082367
http://blog.csdn.net/benjamin_whx/article/details/39082367
http://www.importnew.com/20612.html
欢迎批评指正,共同进步。