《SpringBoot極簡教程》第16章SpringBoot安全整合SpringSecurity
第16章 Spring Boot安全整合Spring Security
開發Web應用,對頁面的安全控制通常是必須的。比如:對於沒有訪問許可權的使用者需要轉到登入表單頁面。要實現訪問控制的方法多種多樣,可以通過Aop、攔截器實現,也可以通過框架實現,例如:Apache Shiro、Spring Security。
很多成熟的大公司都會有專門針對使用者管理方面有一套完整的SSO(單點登入),ACL(許可權訪問控制),UC(使用者中心)系統。 但是在我們開發中小型系統的時候,往往還是優先選擇輕量級可用的業內通用的框架解決方案。
Spring Security 就是一個Spring生態中關於安全方面的框架。它能夠為基於Spring的企業應用系統提供宣告式的安全訪問控制解決方案。
Spring Security,是一個基於Spring AOP和Servlet過濾器的安全框架。它提供全面的安全性解決方案,同時在Web請求級和方法呼叫級處理身份確認和授權。在Spring Framework基礎上,Spring Security充分利用了依賴注入(DI,Dependency Injection)和麵向切面技術。
Spring Security提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC(Inversion of Control, 控制反轉),DI和AOP(Aspect Oriented Progamming ,面向切面程式設計)功能,為應用系統提供宣告式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複程式碼的工作,為基於J2EE企業應用軟體提供了全面安全服務[0]。Spring Security的前身是 Acegi Security 。
本章節使用SpringBoot整合Spring Security開發一個LightSword介面自動化測試平臺,由淺入深的講解SpringBoot整合Spring Security開發技術知識。
本章節採用SpringBoot整合的主要的後端技術框架:
程式語言:java,scala
ORM框架:jpa
View模板引擎:velocity
安全框架:spring security
資料庫:mysql
初階 Security: 預設認證使用者名稱密碼
專案pom.xml新增spring-boot-starter-security依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
重啟你的應用。再次開啟頁面,你講看到一個alert表單對話方塊:
這個使用者名稱,密碼是什麼呢?
讓我們來從SpringBoot原始碼尋找一下。
你搜一下輸出日誌,會看到下面一段輸出:
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類裡面的如下方法輸出的:
@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這個Bean管理了使用者名稱和密碼。
在SecurityProperties裡面的一個內部靜態類User類裡面,管理了預設的認證的使用者名稱與密碼。程式碼如下
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預設的使用者名稱是user, 預設密碼是應用啟動的時候,通過UUID演算法隨機生成的。預設的role是”USER”。
當然,如果我們想簡單改一下這個使用者名稱密碼,可以在application.properties配置你的使用者名稱密碼,例如
# security
security.user.name=admin
security.user.password=admin
當然這只是一個初級的配置,更復雜的配置,可以分不用角色,在控制範圍上,能夠攔截到方法級別的許可權控制。 且看下文分解。
中階 Security:記憶體使用者名稱密碼認證
在上面章節,我們什麼都沒做,就新增了spring-boot-starter-security依賴,整個應用就有了預設的認證安全機制。下面,我們來定製使用者名稱密碼。
寫一個extends WebSecurityConfigurerAdapter的配置類:
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");
}
}
簡要說明:
1.通過 @EnableWebSecurity註解開啟Spring Security的功能。使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個註解,可以開啟security的註解,我們可以在需要控制許可權的方法上面使用@PreAuthorize,@PreFilter這些註解。
2.extends 繼承 WebSecurityConfigurerAdapter 類,並重寫它的方法來設定一些web安全的細節。我們結合@EnableWebSecurity註解和繼承WebSecurityConfigurerAdapter,來給我們的系統加上基於web的安全機制。
3.在configure(HttpSecurity http)方法裡面,預設的認證程式碼是:
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
從方法名我們基本可以看懂這些方法的功能。上面的那個預設的登入頁面,就是SpringBoot預設的使用者名稱密碼認證的login頁面。其原始碼如下:
<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預設的配置super.configure(http),它通過 authorizeRequests() 定義哪些URL需要被保護、哪些不需要被保護。預設配置是所有訪問頁面都需要認證,才可以訪問。
4.通過 formLogin() 定義當需要使用者登入時候,轉到的登入頁面。
5.configureGlobal(AuthenticationManagerBuilder auth) 方法,在記憶體中建立了一個使用者,該使用者的名稱為root,密碼為root,使用者角色為USER。
我們再次啟動應用,訪問 http://localhost:8888
頁面自動跳轉到: http://localhost:8888/login
如下圖所示:
這個預設的登入頁面是怎麼冒出來的呢?是的,SpringBoot內建的,SpringBoot甚至給我們做好了一個極簡的登入頁面。這個登入頁面是通過Filter實現的。具體的實現類是org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter。同時,這個DefaultLoginPageGeneratingFilter也是SpringBoot的預設內建的Filter。
輸入使用者名稱,密碼,點選Login
成功跳轉我們之前要訪問的頁面:
不過,我們發現,SpringBoot應用的啟動日誌還是列印瞭如下一段:
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
但實際上,已經使用了我們定製的使用者名稱密碼了。
如果我們要配置多個使用者,多個角色,可參考使用如下示例的程式碼:
@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");
}
角色許可權控制
當我們的系統功能模組當需求發展到一定程度時,會不同的使用者,不同角色使用我們的系統。這樣就要求我們的系統可以做到,能夠對不同的系統功能模組,開放給對應的擁有其訪問許可權的使用者使用。
Spring Security提供了Spring EL表示式,允許我們在定義URL路徑訪問(@RequestMapping)的方法上面新增註解,來控制訪問許可權。
在標註訪問許可權時,根據對應的表示式返回結果,控制訪問許可權:
true,表示有許可權
fasle,表示無許可權
Spring Security可用表示式物件的基類是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;
}
}
通過閱讀原始碼,我們可以更加深刻的理解其EL寫法,並在寫程式碼的時候正確的使用。變數defaultRolePrefix硬編碼約定了role的字首是”ROLE_”。
同時,我們可以看出hasRole跟hasAnyRole是一樣的。hasAnyRole是呼叫的hasAnyAuthorityName(defaultRolePrefix, roles)。所以,我們在學習一個框架或者一門技術的時候,最準確的就是原始碼。通過原始碼,我們可以更好更深入的理解技術的本質。
SecurityExpressionRoot為我們提供的使用Spring EL表示式總結如下[1]:
表示式 | 描述 |
---|---|
hasRole([role]) | 當前使用者是否擁有指定角色。 |
hasAnyRole([role1,role2]) | 多個角色是一個以逗號進行分隔的字串。如果當前使用者擁有指定角色中的任意一個則返回true。 |
hasAuthority([auth]) | 等同於hasRole |
hasAnyAuthority([auth1,auth2]) | 等同於hasAnyRole |
Principle | 代表當前使用者的principle物件 |
authentication | 直接從SecurityContext獲取的當前Authentication物件 |
permitAll | 總是返回true,表示允許所有的 |
denyAll | 總是返回false,表示拒絕所有的 |
isAnonymous() | 當前使用者是否是一個匿名使用者 |
isRememberMe() | 表示當前使用者是否是通過Remember-Me自動登入的 |
isAuthenticated() | 表示當前使用者是否已經登入認證成功了。 |
isFullyAuthenticated() | 如果當前使用者既不是一個匿名使用者,同時又不是通過Remember-Me自動登入的,則返回true。 |
比如說,在lightsword系統中,我們設定測試報告頁面,只針對ADMIN許可權開放,程式碼如下:
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預設的角色字首是”ROLE_”,使用hasRole方法時已經預設加上了
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")
}
}
然後,我們配置使用者user為USER許可權:
@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");
}
重啟應用,用使用者名稱:user,密碼:user登入系統,訪問/httpreport頁面,我們將會看到如下,不允許訪問的報錯頁面:
簡要說明
在方法上新增@PreAuthorize這個註解,value=”hasRole(`ADMIN`)”)是Spring-EL expression,當表示式值為true,標識這個方法可以被呼叫。如果表示式值是false,標識此方法無許可權訪問。
本小節完整的工程程式碼:
https://github.com/EasySpringBoot/lightsword/tree/spring_security_with_in_memory_auth
在Spring Security裡面獲取當前登入認證通過的使用者資訊
如果我們想要在前端頁面顯示當前登入的使用者怎麼辦呢?在在Spring Security裡面怎樣獲取當前登入認證通過的使用者資訊?下面我們就來探討這個問題。
很好辦。我們新增一個LoginFilter,預設攔截所有請求,把當前登入的使用者放到系統session中即可。在Spring Security中,使用者資訊儲存在SecurityContextHolder中。Spring Security使用一個Authentication物件來持有所有系統的安全認證相關的資訊。這個資訊的內容格式如下:
{
"accountNonExpired":true,
"accountNonLocked":true,
"authorities":[{
"authority":"ROLE_ADMIN"
},{
"authority":"ROLE_USER"
}],
"credentialsNonExpired":true,
"enabled":true,
"username":"root"
}
這個Authentication物件資訊其實就是User實體的資訊(當然,密碼沒放進來)。
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)獲得當前身份驗證的使用者的名稱:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
通過呼叫getContext()返回的物件是SecurityContext的例項物件,該例項物件儲存在ThreadLocal執行緒本地儲存中。使用Spring Security框架,通常的認證機制都是返回UserDetails例項。
Spring MVC的 Web開發使用 Controller 基本上可以完成大部分需求,但是我們還可能會用到 Servlet、Filter、Listener、Interceptor 等等。
在Spring Boot中新增自己的Servlet有兩種方法,程式碼註冊Servlet和註解自動註冊(Filter和Listener也是如此)。
(1)程式碼註冊通過ServletRegistrationBean、 FilterRegistrationBean 和 ServletListenerRegistrationBean 獲得控制。
也可以通過實現 ServletContextInitializer 介面直接註冊。使用程式碼註冊Servlet(就不需要@ServletComponentScan註解)
(2)在 SpringBootApplication 上使用@ServletComponentScan 註解後,Servlet、Filter、Listener 可以直接通過 @WebServlet、@WebFilter、@WebListener 註解自動註冊。
下面我們就採用第(2)種方法,通過新增一個LoginFilter,攔截所有請求,把當前登入資訊放到系統session中,並在前端頁面顯示。
1.新增一個實現了javax.servlet.Filter的LoginFilter,把當前登入資訊放到系統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註解表示執行過濾順序,值越小,越先執行
@WebFilter(filterName = "loginFilter", urlPatterns = Array("/*")) //需要在spring-boot的入口處加註解@ServletComponentScan, 如果不指定,預設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)
}
}
我們通過
val principal = SecurityContextHolder.getContext.getAuthentication.getPrincipal
if (principal.isInstanceOf[UserDetails]) {
username = principal.asInstanceOf[UserDetails].getUsername
}
else {
username = principal.toString
}
拿到認證資訊,然後把使用者名稱放到session中:
session.setAttribute("username", username)
chain.doFilter(request, response)
其中,@WebFilter(filterName = “loginFilter”, urlPatterns = Array(“/“)) ,這個註解用來宣告一個Servlet的Filter,這個加註解@WebFilter的LoginFilter類必須要實現javax.servlet.Filter介面。它會在容器部署的時候掃描處理。如果不指定urlPatterns,預設url-pattern是/。這個@WebFilter註解,在SpringBoot中,要給啟動類加上註解@ServletComponentScan,開啟掃描Servlet元件功能。
2.給啟動類加上註解@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
這個註解將開啟掃描Servlet元件功能。那些被標註了@WebFilter,@WebServlet,@WebListener的Bean將會註冊到容器中。需要注意的一點是,這個掃描動作只在當我們使用的是嵌入式Servlet容器的時候才起作用。完成Bean註冊工作的類是org.springframework.boot.web.servlet.ServletComponentScanRegistrar,它實現了Spring的ImportBeanDefinitionRegistrar介面。
3.前端顯示使用者資訊
Velocity內建了一些物件,例如:$request、$response、$session,這些物件可以在vm模版裡可以直接呼叫。所以我們只需要使用$session取出,當初我們放進session的對應key的屬性值即可。
我們在LoginFilter裡面是這樣放進去的:
session.setAttribute("username", username)
在前端頁面直接這樣取出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.執行測試
部署應用執行,我們看一下執行日誌:
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類成功註冊,路徑對映到/*。同時,我們在
o.s.s.web.DefaultSecurityFilterChain : Creating filter chain:
這行日誌後面,看到SpringBoot預設建立了的那些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使用者名稱登入,我們可以看到頁面上正確展示了我們當前登入的使用者,如下圖
SpringBoot註冊Servlet、Filter、Listener的方法
我們剛才是使用@WebFilter註解一個javax.servlet.Filter的實現類來實現一個LoginFilter。
基於JavaConfig,SpringBoot同樣可以使用如下的方式實現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的自動配置中實現Bean註冊的基本使用方式。
進階 Security: 用資料庫儲存使用者和角色,實現安全認證
本節我們將在我們之前的系統上,實現一個用資料庫儲存使用者和角色,實現系統的安全認證。在許可權角色上,我們簡單設計兩個使用者角色:USER,ADMIN。
我們設計頁面的許可權如下:
首頁/ : 所有人可訪問
登入頁 /login: 所有人可訪問
普通使用者許可權頁 /httpapi, /httpsuite: 登入後的使用者都可訪問
管理員許可權頁 /httpreport : 僅管理員可訪問
無許可權提醒頁: 當一個使用者訪問了其沒有許可權的頁面,我們使用全域性統一的異常處理頁面提示。
1.資料庫層設計:新建三張表User,Role,UserRole
對應的領域實體模型類如下:
使用者表
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 = _
}
使用者角色關聯表
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 = _
}
為了方便測試,我們後面會寫一個使用者測試資料的自動生成的Bean,用來做測試資料的自動初始化工作。
2.配置Spring Security
我們首先使用Spring Security幫我們做登入、登出的處理,以及當使用者未登入時只能訪問: http://localhost:8888/ 以及 http://localhost:8888/login 兩個頁面。
同樣的,我們要寫一個繼承WebSecurityConfigurerAdapter的配置類:
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)
// 這個註解,可以開啟security的註解,我們可以在需要控制許可權的方法上面使用@PreAuthorize,@PreFilter這些註解。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//@Autowired
//LightSwordUserDetailService lightSwordUserDetailService;
@Override
@Bean
public UserDetailsService userDetailsService() { //覆蓋寫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() //預設不攔截靜態資源的url pattern (2)
.anyRequest().authenticated().and()
.formLogin().loginPage("/login")// 登入url請求路徑 (3)
.defaultSuccessUrl("/httpapi").permitAll().and() // 登入成功跳轉路徑url(4)
.logout().permitAll();
http.logout().logoutSuccessUrl("/"); // 退出預設跳轉頁面 (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 來獲取使用者資訊
auth.userDetailsService(userDetailsService()); // (6)
}
}
這裡只做了基本的配置,其中:
(1)覆蓋寫userDetailsService方法,具體的LightSwordUserDetailService實現類,我們下面緊接著會講。
(2)預設不攔截靜態資源的url pattern。我們也可以用下面的WebSecurity這個方式跳過靜態資源的認證
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resourcesDir/**");
}
(3)跳轉登入頁面url請求路徑為/login,我們需要定義一個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 {
/**
* 統一註冊純RequestMapping跳轉View的Controller
*/
override def addViewControllers(registry: ViewControllerRegistry) {
registry.addViewController("/login").setViewName("/login")
}
}
這裡我們直接採用ViewControllerRegistry來註冊一個純路徑對映的Controller方法。
login.html
#parse("/common/header.html")
<div class="container-fluid">
<div class="box box-success">
<div class="box-header">
<h2>LightSword自動化測試平臺(<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>使用者名稱:</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)登入成功後跳轉的路徑為/httpapi
(5)退出後跳轉到的url為/
(6)認證鑑權資訊的Bean,採用我們自定義的從資料庫中獲取使用者資訊的LightSwordUserDetailService類。
我們同樣使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個註解,開啟security的註解,這樣我們可以在需要控制許可權的方法上面使用@PreAuthorize,@PreFilter這些註解。
3.自定義LightSwordUserDetailService
從資料庫中獲取使用者資訊的操作是必不可少的,我們首先來實現UserDetailsService,這個介面需要我們實現一個方法:loadUserByUsername。即從資料庫中取出使用者名稱、密碼以及許可權相關的資訊。最後返回一個UserDetails 實現類。
程式碼如下:
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) // 直接呼叫jpa自動生成的方法
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中呼叫java的collection類,使用scala的foreach,編譯器會提示無法找到result的foreach方法。因為這裡的userRoles的型別為java.util.List。若要將其轉換為Scala的集合,就需要增加如下語句:
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.使用者退出
我們在configure(HttpSecurity http)方法裡面定義了任何許可權都允許退出,
*.logout().permitAll();
http.logout().logoutSuccessUrl("/"); // 退出預設跳轉頁面 (4)
SpringBoot整合Security的預設退出請求是/logout , 我們在頂部導航欄加個退出功能。程式碼如下
<li>
<a href="/logout">
<i class="fa fa-power-off"></i>
</a>
</li>
5.配置錯誤處理頁面
訪問發生錯誤時,跳轉到系統統一異常處理頁面。
我們首先新增一個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])) //表示捕捉到所有的異常,你也可以捕捉一個你自定義的異常
def exception(exception: Exception, request: WebRequest): ModelAndView = {
val modelAndView = new ModelAndView("/error") //error頁面
modelAndView.addObject("errorMessage", exception.getMessage)
modelAndView.addObject("stackTrace", exception.getStackTrace)
modelAndView
}
}
其中,@ExceptionHandler(value = Array(classOf[Exception])) ,表示捕捉到所有的異常,這裡你也可以捕捉一個你自定義的異常。比如說,針對安全認證的Exception,我們可以單獨定義處理。此處不再贅述。感興趣的讀者,可自行嘗試。
錯誤統一處理頁面error.html
#parse("/common/header.html")
<h1>系統異常統一處理頁面</h1>
<h3>異常訊息: $errorMessage</h3>
<h3>異常堆疊資訊:</h3>
<code>
#foreach($e in $stackTrace)
$e
#end
</code>
#parse("/common/footer.html")
6.測試執行
為了方便測試使用者許可權功能,我們給資料庫初始化一些測試資料進去:
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.
* 初始化測試資料
*/
@Service // 需要初始化資料時,開啟註釋即可。
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)
}
}
同樣的,在我們需要許可權控制的頁面對應的方法上新增@PreAuthorize註解,value=”hasRole(`ADMIN`)”)或”hasRole(`USER`)”等。
部署應用,訪問http://localhost:8888/httpapi , 我們可以看到系統自動攔截跳轉到登入頁面
輸入USER角色的使用者名稱jack,密碼123456,系統跳轉到預設登入成功頁面。我們訪問無許可權頁面http://localhost:8888/httpreport ,可以看出,系統攔截到無許可權,跳轉到了錯誤提示頁面
技術點講解
Spring Security 相關介面和類
-
UserDetails 介面:作用是提供認證相關的使用者的資訊. 其主要的方法就是:String getPassword(); 和 String getUsername();
-
User 類: 特指 org.springframework.security.core.userdetails 包中的 User 類。 它實現了 UserDetails 介面。
-
UserDetailsService 介面:作用是在特定使用者許可權認證時,用於載入使用者資訊。 該介面只有一個方法,用於返回使用者的資訊:UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;那麼,它的框架裡面預設的實現類有 InMemoryUserDetailsManager,CachingUserDetailsService 和 JdbcDaoImpl,一個用於從記憶體中拿到使用者資訊,一個用於從資料庫中拿到使用者資訊。
我們自定義LightSwordUserDetailService實現了UserDetailsService介面,從我們自己定義的資料庫表裡面取得使用者資訊來認證鑑權。
小結
本章節通過一個簡單而完整的示例完成了對Web應用的登入,許可權等的安全控制。完整工程原始碼:
https://github.com/EasySpringBoot/lightsword/tree/spring_security_with_db_user_role_2017.4.28
Spring Security提供的功能還遠不止於此,更多Spring Security的使用可參見【參考資料】部分。
參考資料:
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
相關文章
- SpringBoot 整合SpringSecurity JWTSpring BootGseJWT
- SpringBoot 整合 SpringSecurity 梳理Spring BootGse
- SpringBoot極簡整合ShiroSpring Boot
- SpringBoot2.0極簡教程Spring Boot
- SpringBoot框架整合SpringSecurity實現安全訪問控制Spring Boot框架Gse
- SpringBoot整合SpringSecurity(入門級)Spring BootGse
- SpringBoot之整合SpringSecurity,為自己的系統提供安全保障Spring BootGse
- 《Kotlin極簡教程》第6章泛型Kotlin泛型
- 最簡單的SpringBoot整合MyBatis教程Spring BootMyBatis
- 《SpringBoot實戰:從0到1》第1章SpringBoot簡介Spring Boot
- 【springboot】學習4:整合JDBC、整合druid、整合mybatis、整合 SpringSecuritySpring BootJDBCUIMyBatisGse
- 電子商務Java微服務 SpringBoot整合SpringSecurityJava微服務Spring BootGse
- 企業 SpringBoot 教程(六)springboot整合mybatisSpring BootMyBatis
- SpringBoot整合Guacamole教程Spring Boot
- SpringBoot2 整合 SpringSecurity 框架,實現使用者許可權安全管理Spring BootGse框架
- Kafka 簡介 & 整合 SpringBootKafkaSpring Boot
- SpringBoot企業級整合SpringSecurity(WEB+APP+授權)Spring BootGseWebAPP
- SpringBoot簡明教程Spring Boot
- 極速指南:在 SpringBoot 中快速整合騰訊雲簡訊功能Spring Boot
- (七) SpringBoot起飛之路-整合SpringSecurity(Mybatis、JDBC、記憶體)Spring BootGseMyBatisJDBC記憶體
- 在SpringBoot中使用SpringSecuritySpring BootGse
- 企業級 SpringBoot 教程 (二十四)springboot整合dockerSpring BootDocker
- 企業級 SpringBoot 教程 (八)springboot整合spring cacheSpring Boot
- SpringBoot非官方教程 | 第六篇:SpringBoot整合mybatisSpring BootMyBatis
- SpringBoot2-第五章:整合EhCacheSpring Boot
- SpringBoot 整合 SpringSecurity + MySQL + JWT 附原始碼,廢話不多直接盤Spring BootGseMySqlJWT原始碼
- 超詳細教程:SpringBoot整合MybatisPlusSpring BootMyBatis
- SpringBoot(19)---SpringBoot整合ApolloSpring Boot
- SpringBoot(17)---SpringBoot整合RocketMQSpring BootMQ
- SpringBoot(十六)_springboot整合JasperReSpring Boot
- SpringBoot非官方教程 | 第十三篇:springboot整合spring cacheSpring Boot
- RabbitMQ簡介以及與SpringBoot整合示例MQSpring Boot
- SpringBoot2.x版本整合SpringSecurity、Oauth2進行password認證Spring BootGseOAuth
- Springboot整合MybatisPlus(超詳細)完整教程~Spring BootMyBatis
- SpringBoot 整合MyBatis-Plus3.1詳細教程Spring BootMyBatisS3
- dubbo整合springboot最詳細入門教程Spring Boot
- SpringBoot整合系列-整合JPASpring Boot
- SpringBoot 整合 rabbitmqSpring BootMQ