《Spring Boot極簡(jiǎn)教程》第16章 Spring Boot安全集成Spring Security

第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)

  1. UserDetails 接口:作用是提供認(rèn)證相關(guān)的用戶(hù)的信息. 其主要的方法就是:String getPassword(); 和 String getUsername();

  2. User 類(lèi): 特指 org.springframework.security.core.userdetails 包中的 User 類(lèi)。 它實(shí)現(xiàn)了 UserDetails 接口。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評(píng)論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評(píng)論 3 421
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,879評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,181評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,935評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,325評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,534評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,084評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,892評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,322評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,735評(píng)論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,990評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,800評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,084評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,799評(píng)論 18 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,900評(píng)論 6 342
  • 8.6 Spring Boot集成Spring Security 開(kāi)發(fā)Web應(yīng)用,對(duì)頁(yè)面的安全控制通常是必須的。比...
    光劍書(shū)架上的書(shū)閱讀 76,217評(píng)論 5 146
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,503評(píng)論 1 92
  • 指針和引用的區(qū)別: 主要是兩點(diǎn): 引用使用時(shí)必須非空即必須初始化為非空值,指針則初始化時(shí)可以為空; 引用初始化后不...
    geaus閱讀 562評(píng)論 0 0