下面我們將實現關於Spring Security3的一系列教程.
最終的目標是整合Spring Security + Spring3MVC
完成類似於SpringSide3中mini-web的功能.
Spring Security是什麼?
引用
Spring Security,這是一種基於Spring AOP和Servlet過濾器的安全框架。它提供全面的安全性解決方案,同時在Web請求級和方法調用級處理身份確認和授權。在Spring Framework基礎上,Spring Security充分利用了依賴注入(DI,Dependency Injection)和麪向切面技術。
關於Spring Security學習的資料.
最重要,最齊全的中文資料當然是family168的中文文檔
Spring Security2參考文檔
Spring Security3 參考文檔
附件包含了一個很好的初入門的PDF教程.
最好是花30分鐘先照着PDF上的教程一步一步的操作.
雖然沒有實際的應用價值,但對初學者認識SpringSecurity3很有幫助.
我們的項目目錄結構最終是:
需要添加的jar包:
我們先實現一個controller:
MainController.java
package org.liukai.tutorial.controller;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/main")
public class MainController {
protected static Logger logger = Logger.getLogger("controller");
/**
* 跳轉到commonpage頁面
*
* @return
*/
@RequestMapping(value = "/common", method = RequestMethod.GET)
public String getCommonPage() {
logger.debug("Received request to show common page");
return "commonpage";
}
/**
* 跳轉到adminpage頁面
*
* @return
*/
@RequestMapping(value = "/admin", method = RequestMethod.GET)
public String getAadminPage() {
logger.debug("Received request to show admin page");
return "adminpage";
}
}
該controller有兩個mapping映射:
引用
main/common
main/admin
現在我們將同過Spring Security3框架實現成功登陸的人都能訪問到main/common.
但只有擁有admin權限的用戶才能訪問main/admin.
我們先在web.xml中開啓Spring3MVC和SpringSecurity3.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<!-- SpringSecurity必須的filter -->
<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>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring-security.xml
/WEB-INF/applicationContext.xml
</param-value>
</context-param>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
要啓用SpringSecurity3,我們需要完成以下兩步:
1.在web.xml中聲明DelegatingFilterProxy.
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
表示項目中所有路徑的資源都要經過SpringSecurity.
2.導入指定的SpringSecurity配置 :spring-security.xml
關於spring-security.xml的配置.
我們把這個放到後面配置.以便更詳細的講解.
注意一點.最好是將DelegatingFilterProxy寫在DispatcherServlet之前.否則
SpringSecurity可能不會正常工作.
在web.xml中我們定義servlet:spring.
按照慣例,我們必須聲明一個spring-servle.xml
spring-servle.xml
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- 定義一個視圖解析器 -->
<bean id="viewResolver"
class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
</beans>
這個XML配置聲明一個視圖解析器.在控制器中會根據JSP名映射到/ WEB-INF/jsp中相應的位置.
然後創建一個applicationContext.xml.
applicationContext.xml.
<?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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<!-- 激活spring的註解. -->
<context:annotation-config />
<!-- 掃描註解組件並且自動的注入spring beans中.
例如,他會掃描@Controller 和@Service下的文件.所以確保此base-package設置正確. -->
<context:component-scan base-package="org.liukai.tutorial" />
<!-- 配置註解驅動的Spring MVC Controller 的編程模型.注:次標籤只在 Servlet MVC工作! -->
<mvc:annotation-driven />
</beans>
接着是創建JSP頁面
commonpage.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Common Page</h1>
<p>每個人都能訪問的頁面.</p>
<a href="/spring3-security-integration/main/admin"> Go AdminPage </a>
<br />
<a href="/spring3-security-integration/auth/login">退出登錄</a>
</body>
</html>
adminpage.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Admin Page</h1>
<p>管理員頁面</p>
<a href="/spring3-security-integration/auth/login">退出登錄</a>
</body>
</html>
這兩個JSP對應着
當然還有登陸頁面和拒絕訪問頁面
loginpage.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Login</h1>
<div id="login-error">${error}</div>
<form action="../j_spring_security_check" method="post">
<p>
<label for="j_username">Username</label> <input id="j_username"
name="j_username" type="text" />
</p>
<p>
<label for="j_password">Password</label> <input id="j_password"
name="j_password" type="password" />
</p>
<input type="submit" value="Login" />
</form>
</body>
</html>
deniedpage.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>你的權限不夠!</h1>
<p>只有擁有Admin權限才能訪問!</p>
<a href="/spring3-security-integration/auth/login">退出登錄</a>
</body>
</html>
還有一個controller用於映射上面兩個JSP頁面..
LoginLogoutController.java
package org.liukai.tutorial.controller;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("auth")
public class LoginLogoutController {
protected static Logger logger = Logger.getLogger("controller");
/**
* 指向登錄頁面
*/
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String getLoginPage(
@RequestParam(value = "error", required = false) boolean error,
ModelMap model) {
logger.debug("Received request to show login page");
if (error == true) {
// Assign an error message
model.put("error",
"You have entered an invalid username or password!");
} else {
model.put("error", "");
}
return "loginpage";
}
/**
* 指定無訪問額權限頁面
*
* @return
*/
@RequestMapping(value = "/denied", method = RequestMethod.GET)
public String getDeniedPage() {
logger.debug("Received request to show denied page");
return "deniedpage";
}
}
該controller實現了兩個映射
引用
auth/login --顯示Login頁面
auth/denied --顯示拒絕訪問頁面
最後,讓我們看看spring-security.xml的配置
spring-security.xml
<?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-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<!-- Spring-Security 的配置 -->
<!-- 注意開啓use-expressions.表示開啓表達式.
see:http://www.family168.com/tutorial/springsecurity3/html/el-access.html
-->
<security:http auto-config="true" use-expressions="true" access-denied-page="/auth/denied" >
<security:intercept-url pattern="/auth/login" access="permitAll"/>
<security:intercept-url pattern="/main/admin" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/main/common" access="hasRole('ROLE_USER')"/>
<security:form-login
login-page="/auth/login"
authentication-failure-url="/auth/login?error=true"
default-target-url="/main/common"/>
<security:logout
invalidate-session="true"
logout-success-url="/auth/login"
logout-url="/auth/logout"/>
</security:http>
<!-- 指定一個自定義的authentication-manager :customUserDetailsService -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<!-- 對密碼進行MD5編碼 -->
<bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>
<!--
通過 customUserDetailsService,Spring會自動的用戶的訪問級別.
也可以理解成:以後我們和數據庫操作就是通過customUserDetailsService來進行關聯.
-->
<bean id="customUserDetailsService" class="org.liukai.tutorial.service.CustomUserDetailsService"/>
</beans>
在配置中我們可以看到三個URL對應的三個權限
<security:intercept-url pattern="/auth/login" access="permitAll"/>
<security:intercept-url pattern="/main/admin" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/main/common" access="hasRole('ROLE_USER')"/>
需要注意的是我們使用了SpringEL表達式來指定角色的訪問.
以下是表達式對應的用法.
引用
表達式 說明
hasRole([role]) 返回 true 如果當前主體擁有特定角色。
hasAnyRole([role1,role2]) 返回 true 如果當前主體擁有任何一個提供的角色 (使用逗號分隔的字符串隊列)
principal 允許直接訪問主體對象,表示當前用戶
authentication 允許直接訪問當前 Authentication對象 從SecurityContext中獲得
permitAll 一直返回true
denyAll 一直返回false
isAnonymous() 如果用戶是一個匿名登錄的用戶 就會返回 true
isRememberMe() 如果用戶是通過remember-me 登錄的用戶 就會返回 true
isAuthenticated() 如果用戶不是匿名用戶就會返回true
isFullyAuthenticated() 如果用戶不是通過匿名也不是通過remember-me登錄的用戶時, 就會返回true。
所以
<security:intercept-url pattern="/auth/login" access="permitAll"/>
表示所有的人都可以訪問/auth/login.
<security:intercept-url pattern="/main/admin" access="hasRole('ROLE_ADMIN')"/>
<security:intercept-url pattern="/main/common" access="hasRole('ROLE_USER')"/>
則表示只有擁有對應的角色才能訪問.
<security:form-login
login-page="/auth/login"
authentication-failure-url="/auth/login?error=true"
default-target-url="/main/common"/>
表示通過 /auth/login這個映射進行登錄.
如果驗證失敗則返回一個URL:/auth/login?error=true
如果登錄成功則默認指向:/main/common
security:logout
invalidate-session="true"
logout-success-url="/auth/login"
logout-url="/auth/logout"/>
很簡單.我們開啓了session失效功能.
註銷URL爲:/auth/logout
註銷成功後轉向:/auth/login
<!-- 指定一個自定義的authentication-manager :customUserDetailsService -->
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<!-- 對密碼進行MD5編碼 -->
<bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>
<!--
通過 customUserDetailsService,Spring會自動的用戶的訪問級別.
也可以理解成:以後我們和數據庫操作就是通過customUserDetailsService來進行關聯.
-->
<bean id="customUserDetailsService" class="org.liukai.tutorial.service.CustomUserDetailsService"/>
一個自定義的CustomUserDetailsService,是實現SpringSecurity的UserDetailsService接口,但我們重寫了他即便於我們進行數據庫操作.
DbUser.java
package org.liukai.tutorial.domain;
public class DbUser {
private String username;
private String password;
private Integer access;
//getter/setter
}
通過一個初始化的List來模擬數據庫操作.
UserDao.java
package org.liukai.tutorial.dao;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.liukai.tutorial.domain.DbUser;
public class UserDao {
protected static Logger logger = Logger.getLogger("dao");
public DbUser getDatabase(String username) {
List<DbUser> users = internalDatabase();
for (DbUser dbUser : users) {
if (dbUser.getUsername().equals(username) == true) {
logger.debug("User found");
return dbUser;
}
}
logger.error("User does not exist!");
throw new RuntimeException("User does not exist!");
}
/**
* 初始化數據
*/
private List<DbUser> internalDatabase() {
List<DbUser> users = new ArrayList<DbUser>();
DbUser user = null;
user = new DbUser();
user.setUsername("admin");
// "admin"經過MD5加密後
user.setPassword("21232f297a57a5a743894a0e4a801fc3");
user.setAccess(1);
users.add(user);
user = new DbUser();
user.setUsername("user");
// "user"經過MD5加密後
user.setPassword("ee11cbb19052e40b07aac0ca060c23ee");
user.setAccess(2);
users.add(user);
return users;
}
}
自定義UserDetailsService .可以通過繼承UserDetailsService
來達到靈活的自定義UserDetailsService
關於UserDetailsService更多信息. 可以查看SpringSecurity3文檔
CustomUserDetailsService.java
package org.liukai.tutorial.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.log4j.Logger;
import org.liukai.tutorial.dao.UserDao;
import org.liukai.tutorial.domain.DbUser;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
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;
/**
* 一個自定義的service用來和數據庫進行操作. 即以後我們要通過數據庫保存權限.則需要我們繼承UserDetailsService
*
* @author liukai
*
*/
public class CustomUserDetailsService implements UserDetailsService {
protected static Logger logger = Logger.getLogger("service");
private UserDao userDAO = new UserDao();
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
UserDetails user = null;
try {
// 搜索數據庫以匹配用戶登錄名.
// 我們可以通過dao使用JDBC來訪問數據庫
DbUser dbUser = userDAO.getDatabase(username);
// Populate the Spring User object with details from the dbUser
// Here we just pass the username, password, and access level
// getAuthorities() will translate the access level to the correct
// role type
user = new User(dbUser.getUsername(), dbUser.getPassword()
.toLowerCase(), true, true, true, true,
getAuthorities(dbUser.getAccess()));
} catch (Exception e) {
logger.error("Error in retrieving user");
throw new UsernameNotFoundException("Error in retrieving user");
}
return user;
}
/**
* 獲得訪問角色權限
*
* @param access
* @return
*/
public Collection<GrantedAuthority> getAuthorities(Integer access) {
List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>(2);
// 所有的用戶默認擁有ROLE_USER權限
logger.debug("Grant ROLE_USER to this user");
authList.add(new GrantedAuthorityImpl("ROLE_USER"));
// 如果參數access爲1.則擁有ROLE_ADMIN權限
if (access.compareTo(1) == 0) {
logger.debug("Grant ROLE_ADMIN to this user");
authList.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
}
return authList;
}
}
最後啓動服務器輸入:
http://localhost:8080/spring3-security-integration/auth/login
總結
通過本教程.我們對SpringSecurity3有了進一步的認識.
主要是瞭解了UserDetailsService的重要作用.
以及實現了模擬自定義數據的登錄.(這點很重要,很多人學習了SpringSecurity卻不知道
如何自定義權限)
這次教程因爲內容很多,顯得比較粗糙.很多地方並沒有詳細的闡明.
後面的教程還是SpringSecurity.
但我們將對SpringSecurity3新推出的一些特性進行詳細的說明和理解.
BTW:附件爲本次教程源碼.你可以下載後直接在tomcat或其他web服務器啓動.也可以自行添加
maven插件啓動.