第16章 Spring Boot安全集成Spring Security
開(kāi)發(fā)Web應(yīng)用,對(duì)頁(yè)面的安全控制通常是必須的。比如:對(duì)于沒(méi)有訪(fǎng)問(wèn)權(quán)限的用戶(hù)需要轉(zhuǎn)到登錄表單頁(yè)面。要實(shí)現(xiàn)訪(fǎng)問(wèn)控制的方法多種多樣,可以通過(guò)Aop、攔截器實(shí)現(xiàn),也可以通過(guò)框架實(shí)現(xiàn),例如:Apache Shiro、Spring Security。
很多成熟的大公司都會(huì)有專(zhuān)門(mén)針對(duì)用戶(hù)管理方面有一套完整的SSO(單點(diǎn)登錄),ACL(權(quán)限訪(fǎng)問(wèn)控制),UC(用戶(hù)中心)系統(tǒng)。 但是在我們開(kāi)發(fā)中小型系統(tǒng)的時(shí)候,往往還是優(yōu)先選擇輕量級(jí)可用的業(yè)內(nèi)通用的框架解決方案。
Spring Security 就是一個(gè)Spring生態(tài)中關(guān)于安全方面的框架。它能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪(fǎng)問(wèn)控制解決方案。
Spring Security,是一個(gè)基于Spring AOP和Servlet過(guò)濾器的安全框架。它提供全面的安全性解決方案,同時(shí)在Web請(qǐng)求級(jí)和方法調(diào)用級(jí)處理身份確認(rèn)和授權(quán)。在Spring Framework基礎(chǔ)上,Spring Security充分利用了依賴(lài)注入(DI,Dependency Injection)和面向切面技術(shù)。
Spring Security提供了一組可以在Spring應(yīng)用上下文中配置的Bean,充分利用了Spring IoC(Inversion of Control, 控制反轉(zhuǎn)),DI和AOP(Aspect Oriented Progamming ,面向切面編程)功能,為應(yīng)用系統(tǒng)提供聲明式的安全訪(fǎng)問(wèn)控制功能,減少了為企業(yè)系統(tǒng)安全控制編寫(xiě)大量重復(fù)代碼的工作,為基于J2EE企業(yè)應(yīng)用軟件提供了全面安全服務(wù)[0]。Spring Security的前身是 Acegi Security 。
本章節(jié)使用SpringBoot集成Spring Security開(kāi)發(fā)一個(gè)LightSword接口自動(dòng)化測(cè)試平臺(tái),由淺入深的講解SpringBoot集成Spring Security開(kāi)發(fā)技術(shù)知識(shí)。
本章節(jié)采用SpringBoot集成的主要的后端技術(shù)框架:
編程語(yǔ)言:java,scala
ORM框架:jpa
View模板引擎:velocity
安全框架:spring security
數(shù)據(jù)庫(kù):mysql
初階 Security: 默認(rèn)認(rèn)證用戶(hù)名密碼
項(xiàng)目pom.xml添加spring-boot-starter-security依賴(lài)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
重啟你的應(yīng)用。再次打開(kāi)頁(yè)面,你講看到一個(gè)alert表單對(duì)話(huà)框:
這個(gè)用戶(hù)名,密碼是什么呢?
讓我們來(lái)從SpringBoot源碼尋找一下。
你搜一下輸出日志,會(huì)看到下面一段輸出:
2017-04-27 21:39:20.321 INFO 94124 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration :
Using default security password: 6c920ced-f1c1-4604-96f7-f0ce4e46f5d4
這段日志是AuthenticationManagerConfiguration類(lèi)里面的如下方法輸出的:
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
if (auth.isConfigured()) {
return;
}
User user = this.securityProperties.getUser();
if (user.isDefaultPassword()) {
logger.info(String.format("%n%nUsing default security password: %s%n",
user.getPassword()));
}
Set<String> roles = new LinkedHashSet<>(user.getRole());
withUser(user.getName()).password(user.getPassword())
.roles(roles.toArray(new String[roles.size()]));
setField(auth, "defaultUserDetailsService", getUserDetailsService());
super.configure(auth);
}
我們可以看出,是SecurityProperties這個(gè)Bean管理了用戶(hù)名和密碼。
在SecurityProperties里面的一個(gè)內(nèi)部靜態(tài)類(lèi)User類(lèi)里面,管理了默認(rèn)的認(rèn)證的用戶(hù)名與密碼。代碼如下
public static class User {
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
/**
* Granted roles for the default user name.
*/
private List<String> role = new ArrayList<>(Collections.singletonList("USER"));
private boolean defaultPassword = true;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
if (password.startsWith("${") && password.endsWith("}")
|| !StringUtils.hasLength(password)) {
return;
}
this.defaultPassword = false;
this.password = password;
}
public List<String> getRole() {
return this.role;
}
public void setRole(List<String> role) {
this.role = new ArrayList<>(role);
}
public boolean isDefaultPassword() {
return this.defaultPassword;
}
}
綜上所述,security默認(rèn)的用戶(hù)名是user, 默認(rèn)密碼是應(yīng)用啟動(dòng)的時(shí)候,通過(guò)UUID算法隨機(jī)生成的。默認(rèn)的role是"USER"。
當(dāng)然,如果我們想簡(jiǎn)單改一下這個(gè)用戶(hù)名密碼,可以在application.properties配置你的用戶(hù)名密碼,例如
# security
security.user.name=admin
security.user.password=admin
當(dāng)然這只是一個(gè)初級(jí)的配置,更復(fù)雜的配置,可以分不用角色,在控制范圍上,能夠攔截到方法級(jí)別的權(quán)限控制。 且看下文分解。
中階 Security:內(nèi)存用戶(hù)名密碼認(rèn)證
在上面章節(jié),我們什么都沒(méi)做,就添加了spring-boot-starter-security依賴(lài),整個(gè)應(yīng)用就有了默認(rèn)的認(rèn)證安全機(jī)制。下面,我們來(lái)定制用戶(hù)名密碼。
寫(xiě)一個(gè)extends WebSecurityConfigurerAdapter的配置類(lèi):
package com.springboot.in.action.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
/**
* Created by jack on 2017/4/27.
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("root")
.password("root")
.roles("USER");
}
}
簡(jiǎn)要說(shuō)明:
1.通過(guò) @EnableWebSecurity注解開(kāi)啟Spring Security的功能。使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個(gè)注解,可以開(kāi)啟security的注解,我們可以在需要控制權(quán)限的方法上面使用@PreAuthorize,@PreFilter這些注解。
2.extends 繼承 WebSecurityConfigurerAdapter 類(lèi),并重寫(xiě)它的方法來(lái)設(shè)置一些web安全的細(xì)節(jié)。我們結(jié)合@EnableWebSecurity注解和繼承WebSecurityConfigurerAdapter,來(lái)給我們的系統(tǒng)加上基于web的安全機(jī)制。
3.在configure(HttpSecurity http)方法里面,默認(rèn)的認(rèn)證代碼是:
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
從方法名我們基本可以看懂這些方法的功能。上面的那個(gè)默認(rèn)的登錄頁(yè)面,就是SpringBoot默認(rèn)的用戶(hù)名密碼認(rèn)證的login頁(yè)面。其源代碼如下:
<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
<h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
<table>
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
<tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
<input name="_csrf" type="hidden" value="b2155184-80cf-48a2-b547-91bbe364c98e" />
</table>
</form></body></html>
我們使用SpringBoot默認(rèn)的配置super.configure(http),它通過(guò) authorizeRequests() 定義哪些URL需要被保護(hù)、哪些不需要被保護(hù)。默認(rèn)配置是所有訪(fǎng)問(wèn)頁(yè)面都需要認(rèn)證,才可以訪(fǎng)問(wèn)。
4.通過(guò) formLogin() 定義當(dāng)需要用戶(hù)登錄時(shí)候,轉(zhuǎn)到的登錄頁(yè)面。
5.configureGlobal(AuthenticationManagerBuilder auth) 方法,在內(nèi)存中創(chuàng)建了一個(gè)用戶(hù),該用戶(hù)的名稱(chēng)為root,密碼為root,用戶(hù)角色為USER。
我們?cè)俅螁?dòng)應(yīng)用,訪(fǎng)問(wèn) http://localhost:8888
頁(yè)面自動(dòng)跳轉(zhuǎn)到: http://localhost:8888/login
如下圖所示:
這個(gè)默認(rèn)的登錄頁(yè)面是怎么冒出來(lái)的呢?是的,SpringBoot內(nèi)置的,SpringBoot甚至給我們做好了一個(gè)極簡(jiǎn)的登錄頁(yè)面。這個(gè)登錄頁(yè)面是通過(guò)Filter實(shí)現(xiàn)的。具體的實(shí)現(xiàn)類(lèi)是org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter。同時(shí),這個(gè)DefaultLoginPageGeneratingFilter也是SpringBoot的默認(rèn)內(nèi)置的Filter。
輸入用戶(hù)名,密碼,點(diǎn)擊Login
成功跳轉(zhuǎn)我們之前要訪(fǎng)問(wèn)的頁(yè)面:
不過(guò),我們發(fā)現(xiàn),SpringBoot應(yīng)用的啟動(dòng)日志還是打印了如下一段:
2017-04-27 22:51:44.059 INFO 95039 --- [ost-startStop-1] b.a.s.AuthenticationManagerConfiguration :
Using default security password: 5fadfb54-2096-4a0b-ad46-2dad3220c825
但實(shí)際上,已經(jīng)使用了我們定制的用戶(hù)名密碼了。
如果我們要配置多個(gè)用戶(hù),多個(gè)角色,可參考使用如下示例的代碼:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("root")
.password("root")
.roles("USER")
.and()
.withUser("admin").password("admin")
.roles("ADMIN", "USER")
.and()
.withUser("user").password("user")
.roles("USER");
}
角色權(quán)限控制
當(dāng)我們的系統(tǒng)功能模塊當(dāng)需求發(fā)展到一定程度時(shí),會(huì)不同的用戶(hù),不同角色使用我們的系統(tǒng)。這樣就要求我們的系統(tǒng)可以做到,能夠?qū)Σ煌南到y(tǒng)功能模塊,開(kāi)放給對(duì)應(yīng)的擁有其訪(fǎng)問(wèn)權(quán)限的用戶(hù)使用。
Spring Security提供了Spring EL表達(dá)式,允許我們?cè)诙xURL路徑訪(fǎng)問(wèn)(@RequestMapping)的方法上面添加注解,來(lái)控制訪(fǎng)問(wèn)權(quán)限。
在標(biāo)注訪(fǎng)問(wèn)權(quán)限時(shí),根據(jù)對(duì)應(yīng)的表達(dá)式返回結(jié)果,控制訪(fǎng)問(wèn)權(quán)限:
true,表示有權(quán)限
fasle,表示無(wú)權(quán)限
Spring Security可用表達(dá)式對(duì)象的基類(lèi)是SecurityExpressionRoot。
package org.springframework.security.access.expression;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
/**
* Base root object for use in Spring Security expression evaluations.
*
* @author Luke Taylor
* @since 3.0
*/
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
protected final Authentication authentication;
private AuthenticationTrustResolver trustResolver;
private RoleHierarchy roleHierarchy;
private Set<String> roles;
private String defaultRolePrefix = "ROLE_";
/** Allows "permitAll" expression */
public final boolean permitAll = true;
/** Allows "denyAll" expression */
public final boolean denyAll = false;
private PermissionEvaluator permissionEvaluator;
public final String read = "read";
public final String write = "write";
public final String create = "create";
public final String delete = "delete";
public final String admin = "administration";
/**
* Creates a new instance
* @param authentication the {@link Authentication} to use. Cannot be null.
*/
public SecurityExpressionRoot(Authentication authentication) {
if (authentication == null) {
throw new IllegalArgumentException("Authentication object cannot be null");
}
this.authentication = authentication;
}
public final boolean hasAuthority(String authority) {
return hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
public final boolean hasRole(String role) {
return hasAnyRole(role);
}
public final boolean hasAnyRole(String... roles) {
return hasAnyAuthorityName(defaultRolePrefix, roles);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
public final Authentication getAuthentication() {
return authentication;
}
public final boolean permitAll() {
return true;
}
public final boolean denyAll() {
return false;
}
public final boolean isAnonymous() {
return trustResolver.isAnonymous(authentication);
}
public final boolean isAuthenticated() {
return !isAnonymous();
}
public final boolean isRememberMe() {
return trustResolver.isRememberMe(authentication);
}
public final boolean isFullyAuthenticated() {
return !trustResolver.isAnonymous(authentication)
&& !trustResolver.isRememberMe(authentication);
}
/**
* Convenience method to access {@link Authentication#getPrincipal()} from
* {@link #getAuthentication()}
* @return
*/
public Object getPrincipal() {
return authentication.getPrincipal();
}
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
this.trustResolver = trustResolver;
}
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
/**
* <p>
* Sets the default prefix to be added to {@link #hasAnyRole(String...)} or
* {@link #hasRole(String)}. For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN")
* is passed in, then the role ROLE_ADMIN will be used when the defaultRolePrefix is
* "ROLE_" (default).
* </p>
*
* <p>
* If null or empty, then no default role prefix is used.
* </p>
*
* @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_".
*/
public void setDefaultRolePrefix(String defaultRolePrefix) {
this.defaultRolePrefix = defaultRolePrefix;
}
private Set<String> getAuthoritySet() {
if (roles == null) {
roles = new HashSet<String>();
Collection<? extends GrantedAuthority> userAuthorities = authentication
.getAuthorities();
if (roleHierarchy != null) {
userAuthorities = roleHierarchy
.getReachableGrantedAuthorities(userAuthorities);
}
roles = AuthorityUtils.authorityListToSet(userAuthorities);
}
return roles;
}
public boolean hasPermission(Object target, Object permission) {
return permissionEvaluator.hasPermission(authentication, target, permission);
}
public boolean hasPermission(Object targetId, String targetType, Object permission) {
return permissionEvaluator.hasPermission(authentication, (Serializable) targetId,
targetType, permission);
}
public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
}
/**
* Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role
* does not already start with defaultRolePrefix.
*
* @param defaultRolePrefix
* @param role
* @return
*/
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
if (role == null) {
return role;
}
if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
return role;
}
if (role.startsWith(defaultRolePrefix)) {
return role;
}
return defaultRolePrefix + role;
}
}
通過(guò)閱讀源碼,我們可以更加深刻的理解其EL寫(xiě)法,并在寫(xiě)代碼的時(shí)候正確的使用。變量defaultRolePrefix硬編碼約定了role的前綴是"ROLE_"。
同時(shí),我們可以看出hasRole跟hasAnyRole是一樣的。hasAnyRole是調(diào)用的hasAnyAuthorityName(defaultRolePrefix, roles)。所以,我們?cè)趯W(xué)習(xí)一個(gè)框架或者一門(mén)技術(shù)的時(shí)候,最準(zhǔn)確的就是源碼。通過(guò)源碼,我們可以更好更深入的理解技術(shù)的本質(zhì)。
SecurityExpressionRoot為我們提供的使用Spring EL表達(dá)式總結(jié)如下[1]:
表達(dá)式 | 描述 |
---|---|
hasRole([role]) | 當(dāng)前用戶(hù)是否擁有指定角色。 |
hasAnyRole([role1,role2]) | 多個(gè)角色是一個(gè)以逗號(hào)進(jìn)行分隔的字符串。如果當(dāng)前用戶(hù)擁有指定角色中的任意一個(gè)則返回true。 |
hasAuthority([auth]) | 等同于hasRole |
hasAnyAuthority([auth1,auth2]) | 等同于hasAnyRole |
Principle | 代表當(dāng)前用戶(hù)的principle對(duì)象 |
authentication | 直接從SecurityContext獲取的當(dāng)前Authentication對(duì)象 |
permitAll | 總是返回true,表示允許所有的 |
denyAll | 總是返回false,表示拒絕所有的 |
isAnonymous() | 當(dāng)前用戶(hù)是否是一個(gè)匿名用戶(hù) |
isRememberMe() | 表示當(dāng)前用戶(hù)是否是通過(guò)Remember-Me自動(dòng)登錄的 |
isAuthenticated() | 表示當(dāng)前用戶(hù)是否已經(jīng)登錄認(rèn)證成功了。 |
isFullyAuthenticated() | 如果當(dāng)前用戶(hù)既不是一個(gè)匿名用戶(hù),同時(shí)又不是通過(guò)Remember-Me自動(dòng)登錄的,則返回true。 |
比如說(shuō),在lightsword系統(tǒng)中,我們?cè)O(shè)置測(cè)試報(bào)告頁(yè)面,只針對(duì)ADMIN權(quán)限開(kāi)放,代碼如下:
package com.springboot.in.action.controller
import java.util
import com.alibaba.fastjson.serializer.SerializerFeature
import com.springboot.in.action.dao.HttpReportDao
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.{RequestMapping, RestController}
import org.springframework.web.servlet.ModelAndView
import scala.collection.JavaConversions._
@RestController
@RequestMapping(Array("/httpreport"))
class HttpReportController @Autowired()(val HttpReportDao: HttpReportDao) {
@RequestMapping(value = {
Array("", "/")
})
@PreAuthorize("hasRole('ADMIN')") // Spring Security默認(rèn)的角色前綴是”ROLE_”,使用hasRole方法時(shí)已經(jīng)默認(rèn)加上了
def list(model: Model) = {
val reports = HttpReportDao.findAll
model.addAttribute("reports", reports)
val rateList = new util.ArrayList[Double]
val trendList = new util.ArrayList[Object]
for (r <- reports) {
rateList.add(r.rate)
// QualityTrend
val qt = new util.HashMap[String, Any]
qt.put("id", r.id)
qt.put("failed", r.fail)
qt.put("totalCases", r.pass + r.fail)
qt.put("rate", r.rate)
trendList.add(qt)
}
val jsonstr = com.alibaba.fastjson.JSON.toJSONString(trendList, SerializerFeature.BrowserCompatible)
println(jsonstr)
model.addAttribute("rateList", rateList)
model.addAttribute("trendList", jsonstr)
new ModelAndView("/httpreport/list")
}
}
然后,我們配置用戶(hù)user為USER權(quán)限:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("root")
.password("root")
.roles("ADMIN", "USER")
.and()
.withUser("admin").password("admin")
.roles("ADMIN", "USER")
.and()
.withUser("user").password("user")
.roles("USER");
}
重啟應(yīng)用,用用戶(hù)名:user,密碼:user登錄系統(tǒng),訪(fǎng)問(wèn)/httpreport頁(yè)面,我們將會(huì)看到如下,不允許訪(fǎng)問(wèn)的報(bào)錯(cuò)頁(yè)面:
簡(jiǎn)要說(shuō)明
在方法上添加@PreAuthorize這個(gè)注解,value="hasRole('ADMIN')")是Spring-EL expression,當(dāng)表達(dá)式值為true,標(biāo)識(shí)這個(gè)方法可以被調(diào)用。如果表達(dá)式值是false,標(biāo)識(shí)此方法無(wú)權(quán)限訪(fǎng)問(wèn)。
本小節(jié)完整的工程代碼:
https://github.com/EasySpringBoot/lightsword/tree/spring_security_with_in_memory_auth
在Spring Security里面獲取當(dāng)前登錄認(rèn)證通過(guò)的用戶(hù)信息
如果我們想要在前端頁(yè)面顯示當(dāng)前登錄的用戶(hù)怎么辦呢?在在Spring Security里面怎樣獲取當(dāng)前登錄認(rèn)證通過(guò)的用戶(hù)信息?下面我們就來(lái)探討這個(gè)問(wèn)題。
很好辦。我們添加一個(gè)LoginFilter,默認(rèn)攔截所有請(qǐng)求,把當(dāng)前登錄的用戶(hù)放到系統(tǒng)session中即可。在Spring Security中,用戶(hù)信息保存在SecurityContextHolder中。Spring Security使用一個(gè)Authentication對(duì)象來(lái)持有所有系統(tǒng)的安全認(rèn)證相關(guān)的信息。這個(gè)信息的內(nèi)容格式如下:
{
"accountNonExpired":true,
"accountNonLocked":true,
"authorities":[{
"authority":"ROLE_ADMIN"
},{
"authority":"ROLE_USER"
}],
"credentialsNonExpired":true,
"enabled":true,
"username":"root"
}
這個(gè)Authentication對(duì)象信息其實(shí)就是User實(shí)體的信息(當(dāng)然,密碼沒(méi)放進(jìn)來(lái))。
public class User implements UserDetails, CredentialsContainer {
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
....
}
我們可以使用下面的代碼(Java)獲得當(dāng)前身份驗(yàn)證的用戶(hù)的名稱(chēng):
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
通過(guò)調(diào)用getContext()返回的對(duì)象是SecurityContext的實(shí)例對(duì)象,該實(shí)例對(duì)象保存在ThreadLocal線(xiàn)程本地存儲(chǔ)中。使用Spring Security框架,通常的認(rèn)證機(jī)制都是返回UserDetails實(shí)例。
Spring MVC的 Web開(kāi)發(fā)使用 Controller 基本上可以完成大部分需求,但是我們還可能會(huì)用到 Servlet、Filter、Listener、Interceptor 等等。
在Spring Boot中添加自己的Servlet有兩種方法,代碼注冊(cè)Servlet和注解自動(dòng)注冊(cè)(Filter和Listener也是如此)。
(1)代碼注冊(cè)通過(guò)ServletRegistrationBean、 FilterRegistrationBean 和 ServletListenerRegistrationBean 獲得控制。
也可以通過(guò)實(shí)現(xiàn) ServletContextInitializer 接口直接注冊(cè)。使用代碼注冊(cè)Servlet(就不需要@ServletComponentScan注解)
(2)在 SpringBootApplication 上使用@ServletComponentScan 注解后,Servlet、Filter、Listener 可以直接通過(guò) @WebServlet、@WebFilter、@WebListener 注解自動(dòng)注冊(cè)。
下面我們就采用第(2)種方法,通過(guò)添加一個(gè)LoginFilter,攔截所有請(qǐng)求,把當(dāng)前登錄信息放到系統(tǒng)session中,并在前端頁(yè)面顯示。
1.添加一個(gè)實(shí)現(xiàn)了javax.servlet.Filter的LoginFilter,把當(dāng)前登錄信息放到系統(tǒng)session中
代碼如下
package com.springboot.in.action.filter
import javax.servlet._
import javax.servlet.annotation.WebFilter
import javax.servlet.http.HttpServletRequest
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.serializer.SerializerFeature
import org.springframework.core.annotation.Order
/**
* Created by jack on 2017/4/28.
*/
@Order(1) //@Order注解表示執(zhí)行過(guò)濾順序,值越小,越先執(zhí)行
@WebFilter(filterName = "loginFilter", urlPatterns = Array("/*")) //需要在spring-boot的入口處加注解@ServletComponentScan, 如果不指定,默認(rèn)url-pattern是/*
class LoginFilter extends Filter {
override def init(filterConfig: FilterConfig): Unit = {}
override def destroy(): Unit = {}
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
val session = request.asInstanceOf[HttpServletRequest].getSession
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
val principal = SecurityContextHolder.getContext.getAuthentication.getPrincipal
println("LoginFilter:" + JSON.toJSONString(principal, SerializerFeature.PrettyFormat))
var username = ""
if (principal.isInstanceOf[UserDetails]) {
username = principal.asInstanceOf[UserDetails].getUsername
}
else {
username = principal.toString
}
session.setAttribute("username", username)
chain.doFilter(request, response)
}
}
我們通過(guò)
val principal = SecurityContextHolder.getContext.getAuthentication.getPrincipal
if (principal.isInstanceOf[UserDetails]) {
username = principal.asInstanceOf[UserDetails].getUsername
}
else {
username = principal.toString
}
拿到認(rèn)證信息,然后把用戶(hù)名放到session中:
session.setAttribute("username", username)
chain.doFilter(request, response)
其中,@WebFilter(filterName = "loginFilter", urlPatterns = Array("/")) ,這個(gè)注解用來(lái)聲明一個(gè)Servlet的Filter,這個(gè)加注解@WebFilter的LoginFilter類(lèi)必須要實(shí)現(xiàn)javax.servlet.Filter接口。它會(huì)在容器部署的時(shí)候掃描處理。如果不指定urlPatterns,默認(rèn)url-pattern是/。這個(gè)@WebFilter注解,在SpringBoot中,要給啟動(dòng)類(lèi)加上注解@ServletComponentScan,開(kāi)啟掃描Servlet組件功能。
2.給啟動(dòng)類(lèi)加上注解@ServletComponentScan
package com.springboot.in.action
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.web.servlet.ServletComponentScan
@SpringBootApplication
@ServletComponentScan(basePackages = Array("com.springboot.in.action"))
class AppConfig
這個(gè)注解將開(kāi)啟掃描Servlet組件功能。那些被標(biāo)注了@WebFilter,@WebServlet,@WebListener的Bean將會(huì)注冊(cè)到容器中。需要注意的一點(diǎn)是,這個(gè)掃描動(dòng)作只在當(dāng)我們使用的是嵌入式Servlet容器的時(shí)候才起作用。完成Bean注冊(cè)工作的類(lèi)是org.springframework.boot.web.servlet.ServletComponentScanRegistrar,它實(shí)現(xiàn)了Spring的ImportBeanDefinitionRegistrar接口。
3.前端顯示用戶(hù)信息
Velocity內(nèi)置了一些對(duì)象,例如:$request、$response、$session,這些對(duì)象可以在vm模版里可以直接調(diào)用。所以我們只需要使用$session取出,當(dāng)初我們放進(jìn)session的對(duì)應(yīng)key的屬性值即可。
我們?cè)贚oginFilter里面是這樣放進(jìn)去的:
session.setAttribute("username", username)
在前端頁(yè)面直接這樣取出username
<div class="pull-left info">
<p>$session.getAttribute('username')</p>
<a href="#"><i class="fa fa-circle text-success"></i> Online</a>
</div>
4.運(yùn)行測(cè)試
部署應(yīng)用運(yùn)行,我們看一下運(yùn)行日志:
2017-04-28 21:42:46.072 INFO 2961 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8888 (http)
2017-04-28 21:42:46.097 INFO 2961 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2017-04-28 21:42:46.099 INFO 2961 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.0.33
2017-04-28 21:42:46.328 INFO 2961 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2017-04-28 21:42:46.328 INFO 2961 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 4325 ms
2017-04-28 21:42:46.984 INFO 2961 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-04-28 21:42:46.984 INFO 2961 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-04-28 21:42:46.985 INFO 2961 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-04-28 21:42:46.985 INFO 2961 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2017-04-28 21:42:46.987 INFO 2961 --- [ost-startStop-1] .e.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
2017-04-28 21:42:46.988 INFO 2961 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'com.springboot.in.action.filter.LoginFilter' to: [/*]
2017-04-28 21:42:46.988 INFO 2961 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2017-04-28 21:42:47.734 INFO 2961 --- [ost-startStop-1] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7a20e3e2, org.springframework.security.web.context.SecurityContextPersistenceFilter@6d522c58, org.springframework.security.web.header.HeaderWriterFilter@43ba5fdb, org.springframework.security.web.csrf.CsrfFilter@4ae04f7a, org.springframework.security.web.authentication.logout.LogoutFilter@31e3441f, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@30dfa22c, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@605f9361, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@22a03a5f, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7806751c, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@67831f83, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@134ae8c4, org.springframework.security.web.session.SessionManagementFilter@4c60d4b8, org.springframework.security.web.access.ExceptionTranslationFilter@2be01c38, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@34cb7b6d]
2017-04-28 21:42:48.105 INFO 2961 --- [ main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2017-04-28 21:42:48.121 INFO 2961 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [
在上面的日志里面,我們可以看到如下一行
o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'com.springboot.in.action.filter.LoginFilter' to: [/*]
這表明我們定義的LoginFilter類(lèi)成功注冊(cè),路徑映射到/*。同時(shí),我們?cè)?/p>
o.s.s.web.DefaultSecurityFilterChain : Creating filter chain:
這行日志后面,看到SpringBoot默認(rèn)創(chuàng)建了的那些Filter Chain。這些Filter如下:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7a20e3e2,
org.springframework.security.web.context.SecurityContextPersistenceFilter@6d522c58,
org.springframework.security.web.header.HeaderWriterFilter@43ba5fdb,
org.springframework.security.web.csrf.CsrfFilter@4ae04f7a,
org.springframework.security.web.authentication.logout.LogoutFilter@31e3441f,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@30dfa22c,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@605f9361,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@22a03a5f,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7806751c,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@67831f83,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@134ae8c4,
org.springframework.security.web.session.SessionManagementFilter@4c60d4b8,
org.springframework.security.web.access.ExceptionTranslationFilter@2be01c38,
org.springframework.security.web.access.intercept.FilterSecurityInterceptor@34cb7b6d
SpringBoot在背后,為我們默默做了這么多事情。
好了,言歸正傳,我們使用root用戶(hù)名登錄,我們可以看到頁(yè)面上正確展示了我們當(dāng)前登錄的用戶(hù),如下圖
SpringBoot注冊(cè)Servlet、Filter、Listener的方法
我們剛才是使用@WebFilter注解一個(gè)javax.servlet.Filter的實(shí)現(xiàn)類(lèi)來(lái)實(shí)現(xiàn)一個(gè)LoginFilter。
基于JavaConfig,SpringBoot同樣可以使用如下的方式實(shí)現(xiàn)Servlet、Filter、Listener的Bean的配置:
@Configuration
public class WebConfig {
@Bean
public ServletRegistrationBean servletRegistrationBean_demo2(){
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
servletRegistrationBean.addUrlMappings("/demo-servlet");
servletRegistrationBean.setServlet(new DemoServlet());
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new LoginFilter());
Set<String> set = new HashSet<String>();
set.add("/*");
filterRegistrationBean.setUrlPatterns(set);
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean(){
ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new Log4jConfigListener());
servletListenerRegistrationBean.addInitParameter("log4jConfigLocation","classpath:log4j.properties");
return servletListenerRegistrationBean;
}
}
從這里我們可以看出,JavaConfig在SpringBoot的自動(dòng)配置中實(shí)現(xiàn)Bean注冊(cè)的基本使用方式。
進(jìn)階 Security: 用數(shù)據(jù)庫(kù)存儲(chǔ)用戶(hù)和角色,實(shí)現(xiàn)安全認(rèn)證
本節(jié)我們將在我們之前的系統(tǒng)上,實(shí)現(xiàn)一個(gè)用數(shù)據(jù)庫(kù)存儲(chǔ)用戶(hù)和角色,實(shí)現(xiàn)系統(tǒng)的安全認(rèn)證。在權(quán)限角色上,我們簡(jiǎn)單設(shè)計(jì)兩個(gè)用戶(hù)角色:USER,ADMIN。
我們?cè)O(shè)計(jì)頁(yè)面的權(quán)限如下:
首頁(yè)/ : 所有人可訪(fǎng)問(wèn)
登錄頁(yè) /login: 所有人可訪(fǎng)問(wèn)
普通用戶(hù)權(quán)限頁(yè) /httpapi, /httpsuite: 登錄后的用戶(hù)都可訪(fǎng)問(wèn)
管理員權(quán)限頁(yè) /httpreport : 僅管理員可訪(fǎng)問(wèn)
無(wú)權(quán)限提醒頁(yè): 當(dāng)一個(gè)用戶(hù)訪(fǎng)問(wèn)了其沒(méi)有權(quán)限的頁(yè)面,我們使用全局統(tǒng)一的異常處理頁(yè)面提示。
1.數(shù)據(jù)庫(kù)層設(shè)計(jì):新建三張表User,Role,UserRole
對(duì)應(yīng)的領(lǐng)域?qū)嶓w模型類(lèi)如下:
用戶(hù)表
package com.springboot.in.action.entity
import javax.persistence.{Entity, GeneratedValue, GenerationType, Id}
import scala.beans.BeanProperty
/**
* Created by jack on 2017/4/29.
*/
@Entity
class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@BeanProperty
var id: Integer = _
@BeanProperty
var userName: String = _
@BeanProperty
var password: String = _
}
角色表
package com.springboot.in.action.entity
import javax.persistence.{Entity, GeneratedValue, GenerationType, Id}
import scala.beans.BeanProperty
/**
* Created by jack on 2017/4/29.
*/
@Entity
class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@BeanProperty
var id: Integer = _
@BeanProperty
var role: String = _
}
用戶(hù)角色關(guān)聯(lián)表
package com.springboot.in.action.entity
import javax.persistence.{Entity, GeneratedValue, GenerationType, Id}
import scala.beans.BeanProperty
/**
* Created by jack on 2017/4/29.
*/
@Entity
class UserRole {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@BeanProperty
var id: Integer = _
@BeanProperty
var userId: Integer = _
@BeanProperty
var roleId: Integer = _
}
為了方便測(cè)試,我們后面會(huì)寫(xiě)一個(gè)用戶(hù)測(cè)試數(shù)據(jù)的自動(dòng)生成的Bean,用來(lái)做測(cè)試數(shù)據(jù)的自動(dòng)初始化工作。
2.配置Spring Security
我們首先使用Spring Security幫我們做登錄、登出的處理,以及當(dāng)用戶(hù)未登錄時(shí)只能訪(fǎng)問(wèn): http://localhost:8888/ 以及 http://localhost:8888/login 兩個(gè)頁(yè)面。
同樣的,我們要寫(xiě)一個(gè)繼承WebSecurityConfigurerAdapter的配置類(lèi):
package com.springboot.in.action.security;
import com.springboot.in.action.service.LightSwordUserDetailService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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 org.springframework.security.core.userdetails.UserDetailsService;
/**
* Created by jack on 2017/4/27.
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
//使用@EnableGlobalMethodSecurity(prePostEnabled = true)
// 這個(gè)注解,可以開(kāi)啟security的注解,我們可以在需要控制權(quán)限的方法上面使用@PreAuthorize,@PreFilter這些注解。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//@Autowired
//LightSwordUserDetailService lightSwordUserDetailService;
@Override
@Bean
public UserDetailsService userDetailsService() { //覆蓋寫(xiě)userDetailsService方法 (1)
return new LightSwordUserDetailService();
}
/**
* If subclassed this will potentially override subclass configure(HttpSecurity)
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/amchart/**",
"/bootstrap/**",
"/build/**",
"/css/**",
"/dist/**",
"/documentation/**",
"/fonts/**",
"/js/**",
"/pages/**",
"/plugins/**"
).permitAll() //默認(rèn)不攔截靜態(tài)資源的url pattern (2)
.anyRequest().authenticated().and()
.formLogin().loginPage("/login")// 登錄url請(qǐng)求路徑 (3)
.defaultSuccessUrl("/httpapi").permitAll().and() // 登錄成功跳轉(zhuǎn)路徑url(4)
.logout().permitAll();
http.logout().logoutSuccessUrl("/"); // 退出默認(rèn)跳轉(zhuǎn)頁(yè)面 (5)
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//auth
// .inMemoryAuthentication()
// .withUser("root")
// .password("root")
// .roles("ADMIN", "USER")
// .and()
// .withUser("admin").password("admin")
// .roles("ADMIN", "USER")
// .and()
// .withUser("user").password("user")
// .roles("USER");
//AuthenticationManager使用我們的 lightSwordUserDetailService 來(lái)獲取用戶(hù)信息
auth.userDetailsService(userDetailsService()); // (6)
}
}
這里只做了基本的配置,其中:
(1)覆蓋寫(xiě)userDetailsService方法,具體的LightSwordUserDetailService實(shí)現(xiàn)類(lèi),我們下面緊接著會(huì)講。
(2)默認(rèn)不攔截靜態(tài)資源的url pattern。我們也可以用下面的WebSecurity這個(gè)方式跳過(guò)靜態(tài)資源的認(rèn)證
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resourcesDir/**");
}
(3)跳轉(zhuǎn)登錄頁(yè)面url請(qǐng)求路徑為/login,我們需要定義一個(gè)Controller把路徑映射到login.html。代碼如下
package com.springboot.in.action.security
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.{ViewControllerRegistry, WebMvcConfigurerAdapter}
/**
* Created by jack on 2017/4/30.
*/
@Configuration
class WebMvcConfig extends WebMvcConfigurerAdapter {
/**
* 統(tǒng)一注冊(cè)純RequestMapping跳轉(zhuǎn)View的Controller
*/
override def addViewControllers(registry: ViewControllerRegistry) {
registry.addViewController("/login").setViewName("/login")
}
}
這里我們直接采用ViewControllerRegistry來(lái)注冊(cè)一個(gè)純路徑映射的Controller方法。
login.html
#parse("/common/header.html")
<div class="container-fluid">
<div class="box box-success">
<div class="box-header">
<h2>LightSword自動(dòng)化測(cè)試平臺(tái)(<a href="http://localhost:8888/">LightSword</a>)</h2>
</div>
<div class="box-body">
<h3>登錄</h3>
<form name='f' action='/login' method='POST'>
<table>
<tr>
<td>用戶(hù)名:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>密碼:</td>
<td><input type='password' name='password'/></td>
</tr>
<tr>
<td colspan='2'><input name="submit" type="submit" value="登錄"/></td>
</tr>
<!--<input name="_csrf" type="hidden" value="${_csrf}"/>-->
</table>
</form>
</div>
</div>
</div>
<script>
$(function () {
$('[name=f]').focus()
})
</script>
#parse("/common/footer.html")
(4)登錄成功后跳轉(zhuǎn)的路徑為/httpapi
(5)退出后跳轉(zhuǎn)到的url為/
(6)認(rèn)證鑒權(quán)信息的Bean,采用我們自定義的從數(shù)據(jù)庫(kù)中獲取用戶(hù)信息的LightSwordUserDetailService類(lèi)。
我們同樣使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個(gè)注解,開(kāi)啟security的注解,這樣我們可以在需要控制權(quán)限的方法上面使用@PreAuthorize,@PreFilter這些注解。
3.自定義LightSwordUserDetailService
從數(shù)據(jù)庫(kù)中獲取用戶(hù)信息的操作是必不可少的,我們首先來(lái)實(shí)現(xiàn)UserDetailsService,這個(gè)接口需要我們實(shí)現(xiàn)一個(gè)方法:loadUserByUsername。即從數(shù)據(jù)庫(kù)中取出用戶(hù)名、密碼以及權(quán)限相關(guān)的信息。最后返回一個(gè)UserDetails 實(shí)現(xiàn)類(lèi)。
代碼如下:
package com.springboot.in.action.service
import java.util
import com.springboot.in.action.dao.{RoleDao, UserDao, UserRoleDao}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.{User, UserDetails, UserDetailsService, UsernameNotFoundException}
import org.springframework.stereotype.Service
import org.springframework.util.StringUtils
/**
* Created by jack on 2017/4/29.
*/
@Service
class LightSwordUserDetailService extends UserDetailsService {
@Autowired var userRoleDao: UserRoleDao = _
@Autowired var userDao: UserDao = _
@Autowired var roleDao: RoleDao = _
override def loadUserByUsername(username: String): UserDetails = {
// val user = userDao.findByUsername(username) // 直接調(diào)用jpa自動(dòng)生成的方法
val user = userDao.getUserByUsername(username)
if (user == null) throw new UsernameNotFoundException(username + " not found")
val authorities = new util.ArrayList[SimpleGrantedAuthority]
val userRoles = userRoleDao.listByUserId(user.id)
// Scala中調(diào)用java的collection類(lèi),使用scala的foreach,編譯器會(huì)提示無(wú)法找到result的foreach方法。因?yàn)檫@里的userRoles的類(lèi)型為java.util.List。若要將其轉(zhuǎn)換為Scala的集合,就需要增加如下語(yǔ)句:
import scala.collection.JavaConversions._
for (userRole <- userRoles) {
val roleId = userRole.roleId
val roleName = roleDao.findOne(roleId).role
if (!StringUtils.isEmpty(roleName)) {
authorities.add(new SimpleGrantedAuthority(roleName))
}
System.err.println("username is " + username + ", " + roleName)
}
new User(username, user.password, authorities)
}
}
4.用戶(hù)退出
我們?cè)赾onfigure(HttpSecurity http)方法里面定義了任何權(quán)限都允許退出,
*.logout().permitAll();
http.logout().logoutSuccessUrl("/"); // 退出默認(rèn)跳轉(zhuǎn)頁(yè)面 (4)
SpringBoot集成Security的默認(rèn)退出請(qǐng)求是/logout , 我們?cè)陧敳繉?dǎo)航欄加個(gè)退出功能。代碼如下
<li>
<a href="/logout">
<i class="fa fa-power-off"></i>
</a>
</li>
5.配置錯(cuò)誤處理頁(yè)面
訪(fǎng)問(wèn)發(fā)生錯(cuò)誤時(shí),跳轉(zhuǎn)到系統(tǒng)統(tǒng)一異常處理頁(yè)面。
我們首先添加一個(gè)GlobalExceptionHandlerAdvice,使用@ControllerAdvice注解:
package com.springboot.in.action.advice
import org.springframework.web.bind.annotation.{ControllerAdvice, ExceptionHandler}
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.ModelAndView
/**
* Created by jack on 2017/4/27.
*/
@ControllerAdvice
class GlobalExceptionHandlerAdvice {
@ExceptionHandler(value = Array(classOf[Exception])) //表示捕捉到所有的異常,你也可以捕捉一個(gè)你自定義的異常
def exception(exception: Exception, request: WebRequest): ModelAndView = {
val modelAndView = new ModelAndView("/error") //error頁(yè)面
modelAndView.addObject("errorMessage", exception.getMessage)
modelAndView.addObject("stackTrace", exception.getStackTrace)
modelAndView
}
}
其中,@ExceptionHandler(value = Array(classOf[Exception])) ,表示捕捉到所有的異常,這里你也可以捕捉一個(gè)你自定義的異常。比如說(shuō),針對(duì)安全認(rèn)證的Exception,我們可以單獨(dú)定義處理。此處不再贅述。感興趣的讀者,可自行嘗試。
錯(cuò)誤統(tǒng)一處理頁(yè)面error.html
#parse("/common/header.html")
<h1>系統(tǒng)異常統(tǒng)一處理頁(yè)面</h1>
<h3>異常消息: $errorMessage</h3>
<h3>異常堆棧信息:</h3>
<code>
#foreach($e in $stackTrace)
$e
#end
</code>
#parse("/common/footer.html")
6.測(cè)試運(yùn)行
為了方便測(cè)試用戶(hù)權(quán)限功能,我們給數(shù)據(jù)庫(kù)初始化一些測(cè)試數(shù)據(jù)進(jìn)去:
package com.springboot.in.action.service
import java.util.UUID
import javax.annotation.PostConstruct
import com.springboot.in.action.dao.{RoleDao, UserDao, UserRoleDao}
import com.springboot.in.action.entity.{Role, User, UserRole}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
/**
* Created by jack on 2017/4/29.
* 初始化測(cè)試數(shù)據(jù)
*/
@Service // 需要初始化數(shù)據(jù)時(shí),打開(kāi)注釋即可。
class DataInit @Autowired()(val userDao: UserDao,
val userRoleDao: UserRoleDao,
val roleDao: RoleDao) {
@PostConstruct def dataInit(): Unit = {
val uuid = UUID.randomUUID().toString
val admin = new User
val jack = new User
admin.username = "admin_" + uuid
admin.password = "admin"
jack.username = "jack_" + uuid
jack.password = "123456"
userDao.save(admin)
userDao.save(jack)
val adminRole = new Role
val userRole = new Role
adminRole.role = "ROLE_ADMIN"
userRole.role = "ROLE_USER"
roleDao.save(adminRole)
roleDao.save(userRole)
val userRoleAdminRecord1 = new UserRole
userRoleAdminRecord1.userId = admin.id
userRoleAdminRecord1.roleId = adminRole.id
userRoleDao.save(userRoleAdminRecord1)
val userRoleAdminRecord2 = new UserRole
userRoleAdminRecord2.userId = admin.id
userRoleAdminRecord2.roleId = userRole.id
userRoleDao.save(userRoleAdminRecord2)
val userRoleJackRecord = new UserRole
userRoleJackRecord.userId = jack.id
userRoleJackRecord.roleId = userRole.id
userRoleDao.save(userRoleJackRecord)
}
}
同樣的,在我們需要權(quán)限控制的頁(yè)面對(duì)應(yīng)的方法上添加@PreAuthorize注解,value="hasRole('ADMIN')")或"hasRole('USER')"等。
部署應(yīng)用,訪(fǎng)問(wèn)http://localhost:8888/httpapi , 我們可以看到系統(tǒng)自動(dòng)攔截跳轉(zhuǎn)到登錄頁(yè)面
輸入U(xiǎn)SER角色的用戶(hù)名jack,密碼123456,系統(tǒng)跳轉(zhuǎn)到默認(rèn)登錄成功頁(yè)面。我們?cè)L問(wèn)無(wú)權(quán)限頁(yè)面http://localhost:8888/httpreport ,可以看出,系統(tǒng)攔截到無(wú)權(quán)限,跳轉(zhuǎn)到了錯(cuò)誤提示頁(yè)面
技術(shù)點(diǎn)講解
Spring Security 相關(guān)接口和類(lèi)
UserDetails 接口:作用是提供認(rèn)證相關(guān)的用戶(hù)的信息. 其主要的方法就是:String getPassword(); 和 String getUsername();
User 類(lèi): 特指 org.springframework.security.core.userdetails 包中的 User 類(lèi)。 它實(shí)現(xiàn)了 UserDetails 接口。
UserDetailsService 接口:作用是在特定用戶(hù)權(quán)限認(rèn)證時(shí),用于加載用戶(hù)信息。 該接口只有一個(gè)方法,用于返回用戶(hù)的信息:UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;那么,它的框架里面默認(rèn)的實(shí)現(xiàn)類(lèi)有 InMemoryUserDetailsManager,CachingUserDetailsService 和 JdbcDaoImpl,一個(gè)用于從內(nèi)存中拿到用戶(hù)信息,一個(gè)用于從數(shù)據(jù)庫(kù)中拿到用戶(hù)信息。
我們自定義LightSwordUserDetailService實(shí)現(xiàn)了UserDetailsService接口,從我們自己定義的數(shù)據(jù)庫(kù)表里面取得用戶(hù)信息來(lái)認(rèn)證鑒權(quán)。
小結(jié)
本章節(jié)通過(guò)一個(gè)簡(jiǎn)單而完整的示例完成了對(duì)Web應(yīng)用的登錄,權(quán)限等的安全控制。完整工程源代碼:
https://github.com/EasySpringBoot/lightsword/tree/spring_security_with_db_user_role_2017.4.28
Spring Security提供的功能還遠(yuǎn)不止于此,更多Spring Security的使用可參見(jiàn)【參考資料】部分。
參考資料:
0.http://baike.baidu.com/item/spring%20security
1.http://elim.iteye.com/blog/2247073
2.http://blog.csdn.net/u012373815/article/details/54632176
3.https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-secure
4.http://www.open-open.com/lib/view/open1464482054012.html
5.https://github.com/EasySpringBoot/spring-security
6.http://docs.spring.io/spring-security/site/docs/4.1.0.RELEASE/reference/htmlsingle/#jc-authentication
7.https://github.com/pzxwhc/MineKnowContainer/issues/58
8.http://stackoverflow.com/questions/22998731/httpsecurity-websecurity-and-authenticationmanagerbuilder
9.https://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/
10.https://springcloud.cc/spring-security-zhcn.html