《SpringBoot極簡教程》第16章SpringBoot安全整合SpringSecurity

程式設計師詩人發表於2017-04-17

第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 相關介面和類

  1. UserDetails 介面:作用是提供認證相關的使用者的資訊. 其主要的方法就是:String getPassword(); 和 String getUsername();

  2. User 類: 特指 org.springframework.security.core.userdetails 包中的 User 類。 它實現了 UserDetails 介面。

  3. 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


相關文章