spring security4.2.2的maven配置+spring-security配置详解+java源码+数据库设计

时间:2023-03-09 09:52:17
spring security4.2.2的maven配置+spring-security配置详解+java源码+数据库设计

一、引子

最近项目需要添加权限拦截,经讨论决定采用spring security4.2.2!废话少说直接上干货!

spring security 4.2.2文档:http://docs.spring.io/spring-security/site/docs/4.2.2.RELEASE/reference/htmlsingle/#el-access-web

spring security 3 中文2文档:http://www.mossle.com/docs/auth/html/index.html

二、pom.xml配置

需要在pom.xml里配置spring security的依赖,可以通过http://mvnrepository.com/search?q=spring+securitye查询不同版本需要的spring的版本支持。

   <!--spring security  -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>4.2.2.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.2.2.RELEASE</version>
</dependency> <dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>4.2.2.RELEASE</version>
</dependency>

三、web.xml配置

需要在web.xml中配置Spring Security控制权限过滤器,

   <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配置

下面最主要的就是spring-security.xml的配置了

 <?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="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" > <!-- 打印调试信息,仅在开发环境中使用 -->
<!-- <debug/> --> <!-- 不需要被拦截的请求 -->
<http pattern="/loginPage" security="none"/>
<http pattern="/scripts/**" security="none"/> <!--
登录页面可以使用第一种:
<http pattern="/login.jsp" security="none"></http>
<form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" ......
这种方式直接访问.jsp
也可以使用Controller来控制,两种方式登录页面login.jsp的位置不一样!!!
下面是第二种方式:
<http pattern="/login" security="none"></http>
<form-login login-page="/login" login-processing-url="/login" ......
第一个配置告诉spring security,类似于/login的url请求不做过滤处理,而第二个配置信息又告诉spring security url为/login的post请求登录请求处理。正是这种冲突导致了405错误的发生。
既然知道了错误原因,那么只要避免冲突就能解决这个问题:使登录页的请求和登录处理的请求不一致,然后只配置登录页的请求不做拦截处理.
我采用的方法是使用默认的登录URL /login,修改登录页面跳转url为/loginPage。请自行修改代码和配置信息
这样是不是就大工告成了呢?很遗憾,当你启动程序时,输出用户名和密码,不管正确与否,都会有404 Not Found等着你,这又是为什么呢?
登录请求404错误
首先,建议你看一下spring security自动生成的登录页源码,你会发现有如下代码
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
对于什么是csrf,请自行参考网上的资料。
spring security默认情况下csrf protection是开启的,由于我们的登录页没有配置csrf的相关信息,因此spring security内置的过滤器将此链接置为无效链接
解决办法就是配置csrf protection为不可用状态,在配置文件中增加
<csrf disabled="true"/>
--> <!--
1、<http auto-config="true">,他可以自动配置login form,BSIC 认证和logout URL 和logout services,如果没有特殊表明,这个的默认值是false。想要自己配置则设置为"true"
2、Spring Security采用的是一种就近原则,就是说当用户访问的url资源满足多个intercepter-url时,系统将使用第一个符合条件的intercept-url进行权限控制
-->
<http auto-config="true" use-expressions="true">
<!--
另一种权限表达式:
<http use-expressions="false">
<intercept-url pattern='/**' access='ROLE_USER' />
-->
<!-- 禁用CSRF保护,默认是启用 -->
<csrf disabled="true"/> <anonymous enabled="false"/> <!-- 基于角色认证(必须拥有ROLE_XXX角色才能访问所有/**/XXX/**资源) -->
<!-- 确保对功能URL访问都需要权限 -->
<intercept-url pattern="/**/add/**" access="hasRole('ADD')"/>
<intercept-url pattern="/**/update/**" access="hasRole('UPDATE')"/>
<intercept-url pattern="/**/delete/**" access="hasRole('DELETE')"/>
<intercept-url pattern="/**/download/**" access="hasRole('DOWNLOAD')"/>
<intercept-url pattern="/**/access/**" access="hasRole('ADMIN')"/> <!-- Ensures that any request to our application requires the user to be authenticated -->
<intercept-url pattern="/**" access="authenticated"/> <!--
实现免登陆验证,默认有效时间是两周,启用rememberMe之后的两周内,用户都可以直接跳过系统,直接进入系统。
实际上,Spring Security中的rememberMe是依赖cookie实现的,当用户在登录时选择使用rememberMe,系统就会在登录成功后将为用户生成一个唯一标识,并将这个标识保存进cookie中
Spring Security生成的cookie名称是SPRING_SECURITY_REMEMBER_ME_COOKIE,它的内容是一串加密的字符串,
当用户再次访问系统时,Spring Security将从这个cookie读取用户信息,并加以验证。如果可以证实cookie有效,就会自动将用户登录到系统中,并为用户授予对应的权限。
--> <!--
<remember-me remember-me-parameter="remember-me"
data-source-ref="dataSource"/>
spring security还提供了remember me的另一种相对更安全的实现机制 :在客户端的cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在db中保存该加密串-用户信息的对应关系,自动登录时,用cookie中的加密串,到db中验证,如果通过,自动登录才算通过。会自动在你的数据库里创建一个表:PERSISTENT_LOGINS。
如果不加data-source-ref="dataSource",会将上述信息放在内存中!
作者的项目有些特殊要求,所有采用了下面的方式:登录后要在myAuthenticationSuccessHandler做特殊的处理!
-->
<remember-me authentication-success-handler-ref="myAuthenticationSuccessHandler"/> <!--
login-page : 表示用户登陆时显示我们自定义的登录页面
authentication-failure-url : 登录认证失败转向的url,当用户输入的登录名和密码不正确时,系统将再次跳转到登录页面,并添加一个error=true参数作为登陆失败的标示,这个标识是我们自定义的。
default-target-url : 登录认证成功转向的地址
-->
<form-login
login-page="/loginPage"
authentication-failure-url="/loginPage?error=true"
authentication-success-handler-ref="myAuthenticationSuccessHandler"
/> <!-- 登出后,返回到登陆页面 -->
<!-- <logout logout-success-url="/loginPage" logout-url="/logout"/>
delete-cookies="JSESSIONID":退出删除JSESSIONID
-->
<logout />
<!--
控制同步session的过滤器
如果concurrency-control标签配置了error-if-maximum-exceeded="true",max-sessions="1",那么第二次登录时,是登录不了的。
如果error-if-maximum-exceeded="false",那么第二次是能够登录到系统的,
但是第一个登录的账号再次发起请求时,会跳转到expired-url配置的url中,
如果没有配置expired-url,则显示:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
翻译过来意思就是说:这个会话已经过期(可能由于多个并发登录尝试相同的用户)
--> <session-management invalid-session-url="/loginPage" >
<concurrency-control max-sessions="1" error-if-maximum-exceeded="false" expired-url="/loginPage"/>
</session-management> <!-- 指定自己的权限验证过滤器,首先走自己的的过滤器 myFilter,如果被拦截就报没有权限;
如果通过会走spring security自带的拦截器,即上面配置的权限配置!
-->
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="myFilter"/>
</http> <!-- 权限认证Spring日志监听器 -->
<beans:bean class="org.springframework.security.authentication.event.LoggerListener"/>
<beans:bean class="org.springframework.security.access.event.LoggerListener"/> <!--
一个自定义的filter,必须包含authenticationManager,accessDecisionManager,securityMetadataSource三个属性,
我们的所有控制将在这三个类中实现,解释详见具体配置。
-->
<beans:bean id="myFilter" class="com.tcbd.common.interceptor.MyFilterSecurityInterceptor" >
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="accessDecisionManager" ref="myAccessDecisionManagerBean" />
<beans:property name="securityMetadataSource" ref="securityMetadataSource" />
</beans:bean> <!-- 验证配置,实现用户认证的入口,主要实现UserDetailsService接口即可 -->
<authentication-manager alias="authenticationManager" >
<authentication-provider ref="daoAuthenticationProvider" >
</authentication-provider>
</authentication-manager> <beans:bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<beans:property name="userDetailsService" ref="myUserDetailService" />
<beans:property name="passwordEncoder" ref="passwordEncoder" />
</beans:bean>
<!-- spring推荐的单向加密算法 -->
<beans:bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> <!-- 在这个类中,读入用户的密码,角色信息,是否锁定,账号是否过期等属性信息 -->
<beans:bean id="myUserDetailService" class="com.tcbd.common.interceptor.MyUserDetailService" /> <!-- 访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源 -->
<beans:bean id="myAccessDecisionManagerBean" class="com.tcbd.common.interceptor.MyAccessDecisionManager" ></beans:bean> <!-- 资源源数据定义,即定义某一资源可以被哪些角色访问 -->
<beans:bean id="securityMetadataSource" class="com.tcbd.common.interceptor.MyFilterSecurityMetadataSource" /> <beans:bean id="myAuthenticationSuccessHandler" class="com.tcbd.common.interceptor.MyAuthenticationSuccessHandler"/> </beans:beans>

spring security为我们提供了三种通配符。
通配符:?
示例:/admin/g?t.jsp
匹配任意一个字符,/admin/g?t.jsp可以匹配/admin/get.jsp和/admin/got.jsp或是/admin/gxt.do。不能匹配/admin/xxx.jsp。
通配符:*
示例:/admin/*.jsp
匹配任意多个字符,但不能跨越目录。/*/index.jsp可以匹配/admin/index.jsp和/user/index.jsp,但是不能匹配/index.jsp和/user/test/index.jsp。
通配符:**
示例:/**/index.jsp
可以匹配任意多个字符,可以跨越目录,可以匹配/index.jsp,/admin/index.jsp,/user/admin/index.jsp和/a/b/c/d/index.jsp

五、自己的拦截器

1,读入用户的密码,角色信息,是否锁定,账号是否过期等属性信息 :MyUserDetailService

 import java.util.ArrayList;
import java.util.Collection; import javax.annotation.Resource; import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; /**
* 从数据库中读入用户的密码,角色信息,是否锁定,账号是否过期等
*
*/
public class MyUserDetailService implements UserDetailsService {
@Resource
private UserService userService; /**
* 数据库交互获取用户拥有的权限角色,并设置权限
*/
@Override
public UserDetails loadUserByUsername(String userCode) throws UsernameNotFoundException, DataAccessException {
// 根据登录用户名获取用户信息
Users user = new Users();
user.setUserCode(username);
user = userService.selectByModel(user);
if (null != user) {
// 存放权限
Collection<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
String action = user.getRole();
if (StringUtils.isNotBlank(action)) {
String[] roleaCtion = action.split(",");
for (int i = 0; i < roleaCtion.length; i++) {
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(roleaCtion[i]);
auths.add(auth);
}
}
//spring security自带的User对象
User userDetails = new User(username, user.getPassword(), true, true, true, true, auths);
return userDetails;
}
return null;
}
}

2,定义某一资源可以被哪些角色访问:MyFilterSecurityMetadataSource

 import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map; import javax.servlet.http.HttpServletRequest; import org.apache.log4j.Logger;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private static Logger logger = Logger.getLogger(MyFilterSecurityMetadataSource.class); public List<ConfigAttribute> getAttributes(Object object) {
FilterInvocation fi = (FilterInvocation) object;
HttpServletRequest request = fi.getRequest();
String requestUrl = fi.getRequest().getRequestURI();
List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();
// 所有URL对应的角色,应用启动就存放到静态资源里,得到的结果是:不同的URL下,包含的多个角色
Map<String, String> resRoles = Constant.URL_ROLES; for (Map.Entry<String, String> ent : resRoles.entrySet()) {
String url = ent.getKey();
String roles = ent.getValue();
//根据业务写自己的匹配逻辑
if(requestUrl.startsWith(url)){
attributes.addAll(SecurityConfig.createListFromCommaDelimitedString(roles));
}
}
logger.debug("【"+request.getRequestURI()+"】 roles: "+attributes);
return attributes;
} public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
} public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}

3,访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源:MyAccessDecisionManager

 import java.util.Collection;
import java.util.Iterator; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; /**
* 在这种方法中,需要与configAttributes比较验证
* 1、一个对象是一个URL,一个过滤器被这个URL找到权限配置,并通过这里
* 2、如果没有匹配相应的认证,AccessDeniedException
*
*/
public class MyAccessDecisionManager implements AccessDecisionManager { private static final Log logger = LogFactory.getLog(MyAccessDecisionManager.class); /**
* 在这个类中,最重要的是decide方法,如果不存在对该资源的定义,直接放行; 否则,如果找到正确的角色,即认为拥有权限,并放行,否则throw
* new AccessDeniedException("no right");这样,就会进入上面提到的/accessDenied.jsp页面。
* @param authentication :当前用户所且有的角色
* @param object :当前请求的URL
* @param configAttributes :当前URL所且有的角色
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
// 资源所需的角色列表,如果角色列表为空,则放行!继续下一个拦截器。
if (configAttributes == null) {
return;
}
// 即将访问的资源URL,如 : /admin.jsp
logger.info("URL :"+object);
// 遍历所需的角色集合
Iterator<ConfigAttribute> ite = configAttributes.iterator();
while (ite.hasNext()) {
ConfigAttribute ca = ite.next();
// 该资源所需要的角色
String needRole = ((SecurityConfig) ca).getAttribute();
// authentication.getAuthorities()获取用户所拥有的角色列表,如:OLE_DEFULT
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
// 将资源所需要的角色与用户拥有的角色比较
if (needRole.equals(grantedAuthority.getAuthority())) {
// grantedAuthority is user's role.
// 角色相同,直接放行
return;
}
}
}
// 否则,提示没有权限访问该资源
throw new AccessDeniedException("no right");
} @Override
public boolean supports(ConfigAttribute attribute) {
return true;
} @Override
public boolean supports(Class<?> clazz) {
return true;
} }

4,拦截器核心,MyFilterSecurityInterceptor

 import java.io.IOException;

 import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; 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; /**
*
*/
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor
implements Filter { private FilterInvocationSecurityMetadataSource securityMetadataSource; /* get、set方法 */
public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() {
return securityMetadataSource;
} public void setSecurityMetadataSource(
FilterInvocationSecurityMetadataSource securityMetadataSource) {
this.securityMetadataSource = securityMetadataSource;
} @Override
public Class<? extends Object> getSecureObjectClass() {
return FilterInvocation.class;
} @Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
} @Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
} public void invoke(FilterInvocation fi) throws IOException,
ServletException {
/**
* 最核心的代码就是@link InterceptorStatusToken token = super.beforeInvocation(fi);
* 它会调用我们定义的MyInvocationSecurityMetadataSource.getAttributes方法和MyAccessDecisionManager.decide方法
* 这一句,即在执行doFilter之前,进行权限的检查,而具体的实现已经交给@link MyAccessDecisionManager 了
*/
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//继续走下一个拦截器,也就是org.springframework.security.web.access.intercept.FilterSecurityInterceptor
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
} @Override
public void destroy() { } @Override
public void init(FilterConfig arg0) throws ServletException { } }

5,登录页面(需要自己改一些东西)

 <%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix='fmt' uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<meta name="description" content="">
<meta name="author" content="">
<link rel="icon" href="/favicon.ico"> <title><fmt:message key="application.title"></fmt:message></title> <!-- Bootstrap core CSS -->
<link href="/scripts/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- Custom styles for this template --> <link href="/scripts/apps/css/signin.css" rel="stylesheet"> <style type="text/css">
.tip {
font-size: 10px;
color: red;
text-align: center;
} </style>
</head> <body>
<div class="container">
<c:url var="loginUrl" value="/login" />
<form action="${loginUrl}" id="login_form" class="form-signin" method="post">
<div class="form-signin-heading text-center">
<h2 ><fmt:message key="application.title"></fmt:message></h2>
</div>
<div id="tip" class="tip"></div>
<c:if test="${param.error != null}">
<span style="color:red">用户名或密码有误</span>
</c:if> <label for="inputEmail" class="sr-only">登录帐号</label>
<input type="text" id="inputUserCode" name="username" class="form-control" placeholder="登录帐号" autofocus>
<label for="inputPassword" class="sr-only">登录密码</label>
<input type="password" id="inputPassword" name="password" class="form-control" placeholder="登录密码">
<div class="input-group input-sm">
<div class="checkbox">
<label><input type="checkbox" id="rememberme" name="remember-me" value="true"> 下次自动登录</label>
</div>
</div>
<button id="login_button" class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
</form> </div> <script src="/scripts/plugins/jQuery/jquery-2.2.3.min.js"></script>
</body>
</html>

在JSP中相应的操作按钮上加上如下标签,控制显示:

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> 该标签一定要加上!!!
<sec:authorize access="hasRole('ADD')">
<a href="/XXX/add">增加</a>
</sec:authorize>

数据库设计:user表里有roleId,role表,resource-role表,resource表