SpringSecurity基础
Spring Security是什么?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security能做什么?
Spring Security 提供了一套完善的java权限管理体系,从登陆到权限的验证,而且它很好的发挥了spring的特点(基于配置),方便管理维护,在web研发过程中非常的实用。
简单的说SpringSecurity主要做两件事 1.认证 2.授权
<!--more-->
Spring Security如何使用?
因为Spring Security有很多使用方式,所以我这里只讲开发中经常用的自定义的方式,也就是最灵活的方式
首先创建一个Spring MVC项目,为什么不用SpringBoot? 因为我觉得 既然都用上Spring Security 基本不会用SpringBoot。
在项目中加入需要的依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>3.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>3.2.4.RELEASE</version>
</dependency>
在Web.xml中加入SpringSecurity的拦截器
<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>
如果你是像我一样自定义配置文件路径的话也要在Web.xml重写ContextLoaderListener
<!--添加前端控制器-->
<!--添加ContextLoaderListener ContextLoaderListener默认会加载/WEB-Info/applicationContext.xml这个Spring配置文件-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--重写默认配置实现记载多配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:Spring-Security.xml,
classpath:Spring-resources.xml
</param-value>
</context-param>
创建 登陆页 错误页 还有受保护页(可以理解为 登陆后才可以进入页面)
以下是 登陆页 错误页 主页
@RequestMapping("/login")
public String toLogin(){
System.out.println("登陆");
System.out.println();
return "tologin";
}
@RequestMapping("/sorry")
public String toSorry(){
System.out.println("sorry");
System.out.println();
return "sorry";
}
@RequestMapping("index")
public String toIndex(){
System.out.println("index");
System.out.println();
return "index";
}
受保护资源1 admin 才能访问sayHi
@Controller
@RequestMapping("/admin")
public class TestController {
@RequestMapping("/sayHi")
public String toTestJSP(Map<String,Object> map){
String sayHi="Hi,My is Spring MVC";
map.put("sayHi",sayHi);
return "test";
}
}
受保护资源2 user才能访问的sayHi
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/sayHi")
public String toTestJSP(Map<String,Object> map){
return "user";
}
}
至此为止 一切都已经准备好了,下面是SpringSecurity内容 为什么不用数据库,因为我懒得用数据库了身份信息都用死值定义的。
创建Spring-Security.xml
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd">
讲Beans 定义为父元素 不然每次都要写太麻烦了
<!--登陆页不拦截-->
<http pattern="/login" security="none" />
<!--拦截 admin 所有请求-->
<http access-denied-page="/sorry">
<form-login
login-page="/login"
default-target-url="/login"
authentication-failure-url="/login?login_error=t" />
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="false"/>
</session-management>
<!-- <remember-me key="elim" user-service-ref="securityManager"/> -->
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="securityInterceptor"/>
</http>
<beans:bean id="securityInterceptor" class="filter.MyFilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="accessDecisionManager" ref="myAccessDecisionManagerBean" />
<beans:property name="securityMetadataSource" ref="securityMetadataSource" />
</beans:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="msecurityManager">
<!--如果用户的密码采用加密的话 <password-encoder hash="md5" /> -->
</authentication-provider>
</authentication-manager>
<!--在这个类中,你就可以从数据库中读入用户的密码,角色信息,是否锁定,账号是否过期等 -->
<beans:bean id="msecurityManager" class="security.SecurityManagerSupport"/>
<!--访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源 -->
<beans:bean id="myAccessDecisionManagerBean"
class="security.MyAccessDecisionManager">
</beans:bean>
<!--资源源数据定义,将所有的资源和权限对应关系建立起来,即定义某一资源可以被哪些角色访问 -->
<beans:bean id="securityMetadataSource"
class="security.MyInvocationSecurityMetadataSource" />
以上是Spring Security配置文件
access-denied-page 当权限不足跳转的页面
login-page 登陆页
default-target-url 默认页
authentication-failure-url 密码失败跳转的页面(我这里密码失败跳回登陆页 携带额外参数 页面判断是否有额外参数 从而提示用户是否密码错误)
session-management 中可以定义很多元素比如 检测Session超时 concurrency-control session 固定攻击保护
error-if-maximum-exceeded 若当前maximumSessions为1,当设置为true表示同一账户登录会抛出SessionAuthenticationException异常,异常信息为:Maximum sessions of {0} for this principal exceeded
custom-filter Spring Security 的底层是通过一系列的 Filter 来管理的,每个 Filter 都有其自身的功能,而且各个 Filter 在功能上还有关联关系,所以它们的顺序也是非常重要的。
通过 position、before 或者 after 指定该 Filter 放置的位置
FILTER_SECURITY_INTERCEPTOR代表Http
具体如下
别名 | Filter 类 | 对应元素或属性 |
---|---|---|
CHANNEL_FILTER | ChannelProcessingFilter | http/intercept-url@requires-channel |
SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | http |
CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | http/session-management/concurrency-control |
LOGOUT_FILTER | LogoutFilter | http/logout |
X509_FILTER | X509AuthenticationFilter | http/x509 |
PRE_AUTH_FILTER | AstractPreAuthenticatedProcessingFilter 的子类 | 无 |
CAS_FILTER | CasAuthenticationFilter | 无 |
FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | http/form-login |
BASIC_AUTH_FILTER | BasicAuthenticationFilter | http/http-basic |
SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareRequestFilter | http@servlet-api-provision |
JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | http@jaas-api-provision |
REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | http/remember-me |
ANONYMOUS_FILTER | AnonymousAuthenticationFilter | http/anonymous |
SESSION_MANAGEMENT_FILTER | SessionManagementFilter | http/session-management |
EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | http |
FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | http |
SWITCH_USER_FILTER | SwitchUserFilter | 无 |
<beans:bean id="securityInterceptor" class="filter.MyFilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="accessDecisionManager" ref="myAccessDecisionManagerBean" />
<beans:property name="securityMetadataSource" ref="securityMetadataSource" />
</beans:bean>
这段配置文件相信熟悉Spring的人已经明白 将几个对象手动填充到类中
package filter;
import javax.servlet.*;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import java.io.IOException;
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter{
private FilterInvocationSecurityMetadataSource securityMetadataSource;
Log log= LogFactory.getLog(this.getClass());
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return securityMetadataSource;
}
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource) {
this.securityMetadataSource = securityMetadataSource;
}
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
//登陆后,每次访问资源都通过这个拦截器拦截
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
private void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
//它首先会调用MyInvocationSecurityMetadataSource类的getAttributes方法获取被拦截url所需的权限,在调用MyAccessDecisionManager类decide方法判断用户是否够权限
System.out.println("===Fi="+fi.getFullRequestUrl()+"===");
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
public void init(FilterConfig filterConfig) throws ServletException {
}
public void destroy() {
}
}
这个Filter是我们的核心 拦截器根据URL 拦截用户是否有该权限
解释这三个 对象都是做什么的
1.authenticationManager
定义用户信息 角色信息 账号是否过期等
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="msecurityManager">
<!--如果用户的密码采用加密的话 <password-encoder hash="md5" /> -->
</authentication-provider>
</authentication-manager>
<!--在这个类中,你就可以从数据库中读入用户的密码,角色信息,是否锁定,账号是否过期等 -->
<beans:bean id="msecurityManager" class="security.SecurityManagerSupport"/>
也就是这一段
public class SecurityManagerSupport implements UserDetailsService {
//登陆验证时,通过username获取用户的所有权限信息,
//并返回User放到spring的全局缓存SecurityContextHolder中,以供授权器使用
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//因为太麻烦 所以不打算连数据库
List<GrantedAuthority> auths=new ArrayList<GrantedAuthority>();
//模拟从数据库中取出对必须是ROLE_开头的,不然spring security不认账的
SimpleGrantedAuthority auth1=new SimpleGrantedAuthority("ROLE_ADMIN");
SimpleGrantedAuthority auth2=new SimpleGrantedAuthority("ROLE_USER");
System.out.println("===userName"+username+"===");
if(username.equals("admin")){
auths=new ArrayList<GrantedAuthority>();
auths.add(auth1);
}else if(username.equals("user")){
auths=new ArrayList<GrantedAuthority>();
auths.add(auth2);
}
// 账号 密码
User user=new User(username,"yanhao",true,true,true,true,auths);
return user;
}
}
我这里 手动创建了两个角色 User和Admin 如果有数据库的话这里可以做成一个SQL 通过用户名查询 用户的权限 并将这个用户return
定义完 角色权限信息 接下来是定义 某一个权限 能访问那些资源 securityMetadataSource
public class MyInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource{
Log log = LogFactory.getLog(this.getClass());
private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
public MyInvocationSecurityMetadataSource() {
loadResourceDefine();
}
private PathMatcher matcher;
private void loadResourceDefine() {
matcher= new AntPathMatcher();
resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>();
ConfigAttribute ca = new SecurityConfig("ROLE_ADMIN");
atts.add(ca);
resourceMap.put("/admin/sayHi", atts);
Collection<ConfigAttribute> attsno =new ArrayList<ConfigAttribute>();
ConfigAttribute cano = new SecurityConfig("ROLE_USER");
attsno.add(cano);
resourceMap.put("/user/sayHi", attsno);
}
//参数是要访问的url,返回这个url对于的所有权限(或角色)
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 将参数转为url
String url = ((FilterInvocation)object).getRequestUrl();
Iterator<String> ite = resourceMap.keySet().iterator();
while (ite.hasNext()) {
String resURL = ite.next();
System.out.println("===URL="+url+"RESURL="+resURL+"===");
if (matcher.match(url, resURL)) {
System.out.println("Yes Yes Yes");
return resourceMap.get(resURL);
}
}
System.out.println("No No No");
return null;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public boolean supports(Class<?> aClass) {
return true;
}
}
这里我在构造函数中 定义了 权限 admin能访问admin/sayHi User可以访问User/sayHi
实现 getAttributes 判断请求的URL 是否是定义的
最后就剩下accessDecisionManager访问决策器 定义完了 用户信息 和身份信息 最后就差判断了
public class MyAccessDecisionManager implements AccessDecisionManager{
Log log= LogFactory.getLog(this.getClass());
//检查用户是否够权限访问资源
//参数authentication是从spring的全局缓存SecurityContextHolder中拿到的,里面是用户的权限信息
//参数object是url
//参数configAttributes所需的权限
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
if (collection == null) {
return;
}
Iterator<ConfigAttribute> ite = collection.iterator();
while (ite.hasNext()) {
ConfigAttribute ca = ite.next();
String needRole = ((SecurityConfig) ca).getAttribute();
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (needRole.equals(ga.getAuthority())) {
return;
}
}
}
//注意:执行这里,后台是会抛异常的,但是界面会跳转到所配的access-denied-page页面
throw new AccessDeniedException("no right");
}
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
public boolean supports(Class<?> aClass) {
return true;
}
}
这里就是 判断访问的URL 是否是该用户拥有的资源
到此为止 SpringSecurtiy就配置完成了
大致流程就是 用户访问URL 进入拦截器,拦截器 拦截用户请求 判断用户身份是否通过,如果通过判断用户所访问的资源 是否是 有权访问的,如果是 继续执行其它拦截器 如果不是 跳转到error页