Spring Security自定義認證頁面(動態網頁解決方案+靜態網頁解決方案)--練氣中期圓滿


寫在前面

上一回我們簡單分析了spring security攔截器鏈的加載流程,我們還有一些簡單的問題沒有解決。如何自定義登錄頁面?如何通過數據庫獲取用戶權限信息?
今天主要解決如何配置自定義認證頁面的問題。因為現在前后端分離,無狀態、restful接口設計比較火,因此在思考靜態網頁如何獲取spring security的CRSF Token.這個問題我在文末提出了我的見解,但似乎也不是很好的解決方案,很期待大家的寶貴建議!

Spring Security配置自定義認證頁面步驟

第一步:在spring security的配置文件中指定自定義登錄頁面的信息

<!--靜態資源不攔截-->
    <security:http pattern="/assets/**" security="none"/>
<!--設置可以用spring的el表達式配置Spring Security並自動生成對應配置組件(過濾器)-->
    <security:http auto-config="true" use-expressions="true">
        <!--使用spring的el表達式來指定項目所有資源訪問都必須有ROLE_USER或ROLE_ADMIN角色-->
        <security:intercept-url pattern="/**" access="hasAnyRole('ROLE_USER','ROLE_ADMIN')"/>

        <!--        使這個頁面能匿名訪問-->
        <security:intercept-url pattern="/login.html" access="permitAll()"/>
        <security:form-login
                login-page="/login.html"
                login-processing-url="/login"
                default-target-url="/pages/index.html"
                username-parameter="username"
                password-parameter="password"
                authentication-failure-url="/failure.html"/>
        <security:logout
                logout-url="/logout"
                logout-success-url=":/login.html"/>
    </security:http>

關於配置文件的幾點說明:

  1. login-page:配置自定義的認證頁面;
  2. login-processing-url:配置認證處理的url;
  3. default-target-url:配置認證成功之后跳轉的頁面;
  4. authentication-failure-url:配置認證失敗之后跳轉的頁面;
  5. logout-url:配置注銷處理路徑;
  6. logout-success-url:配置成功注銷后跳轉的頁面;
  7. username-parameter:配置前端表單向后台提交用戶賬戶的參數名,默認值為username,可以不配置;
  8. password-parameter:配置前端表單向后台提交用戶憑證的參數名,默認值為password。
  9. security="none"表示該路徑下的資源訪問不攔截(這個和匿名訪問的配置是有區別的)

第二步:編寫一個前端登錄頁面login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
</head>
<!-- 引入樣式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.2/lib/theme-chalk/index.css">
<style>
    * {margin: 0; padding: 0; box-sizing: border-box;}
    .form {position: relative;}
    .son {position: relative; top:50%;margin-top:-50px;left:50%;margin-left:-50px}
</style>
<body>
<div id="app">
<el-container>
    <el-header></el-header>
    <el-main style="margin-top: 10%">

        <div id="form" style="width: 310px;height:280px;margin: auto;background-color: #71d3bd">


            <el-form id="son" ref="form" :model="user"  style="margin: auto;padding-left: 10px;padding-right: 10px">
                <el-form-item>
                    <h3 style="text-align: center">歡迎登錄</h3>
                </el-form-item>
                <el-form-item  style="width: auto;">
                    <el-input v-model="user.username" placeholder="username"></el-input>
                </el-form-item>
                <el-form-item  style="width: auto;">
                    <el-input v-model="user.password" placeholder="password"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="onSubmit" style="width:100%;">登錄</el-button>
                </el-form-item>
            </el-form>
        </div>
    </el-main>
</el-container>
</div>


<!--axios-->
<script src="https://cdn.bootcss.com/axios/0.19.0/axios.min.js"></script>
<!--vue-->
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
<!-- 引入組件庫 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

<script>

    new Vue({
        el: "#app",
        data: function (){
            return {
                user:{
                    username:"",
                    password:""
                }
            }
        },
        methods:{
         onSubmit(){
             axios.post("/login",this.user)
            }
        }

    })
</script>
</body>
</html>

第三步,啟動tomcat,訪問http://127.0.0.1:8081/

我們可以發現,我們被重定向到了登錄頁面。接下來我們填寫username和password點擊登錄(在前面的配置文件中定義了)

我們發現結果並不符合我們的預期。我們得到了403(沒有權限)錯誤信息。

why?

我們對比一下我們寫的登錄頁面和spring security給出的頁面有啥不一樣

<!DOCTYPE html>
<html lang="en"><head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous">
  <style>@media print {#ghostery-purple-box {display:none !important}}</style></head>
  <body>
     <div class="container">
      <form class="form-signin" method="post" action="/login">
        <h2 class="form-signin-heading">Please sign in</h2>
        <p>
          <label for="username" class="sr-only">Username</label>
          <input type="text" id="username" name="username" class="form-control" placeholder="Username" required="" autofocus="">
        </p>
        <p>
          <label for="password" class="sr-only">Password</label>
          <input type="password" id="password" name="password" class="form-control" placeholder="Password" required="">
        </p>
<input name="_csrf" type="hidden" value="0aef3732-50d3-455a-8404-43d5ed9f2939">
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      </form>
</div>
</body></html>

對比發現系統給出的和我們寫的認證頁面在表單中多出了下面這段

<input name="_csrf" type="hidden" value="0aef3732-50d3-455a-8404-43d5ed9f2939">

也就是說我們少了一個參數信息,這參數信息就是我們前篇文章講到的spring security預防csrf攻擊的內容。顯然它是通過校驗token去預防的。

解釋一下如何通過檢驗token的方式預防csrf攻擊:

​ 簡單來說就是趙六你去賬房領銀兩辦事(獲取系統服務)前先向王員外(可以是系統專門發放令牌的一個接口)索要一個令牌(token),你領銀兩的時候帶上王員外給你的令牌,這賬房管事的李老先生(一個攔截器)查看了你的令牌后才能給你錢,不然就不給你錢。你拿錢了之后呢令牌就被收走了(token過期策略),下次再想拿錢就得找王員外再要一個新的令牌。

接下來就讓我們找李老先生拿銀兩去,哦,不對,是找王員外拿雞毛令箭去。。。

第四步:配置csrf攻擊防護機制

從Spring Security 4.0開始,默認情況下使用XML配置啟用CSRF保護。如果要禁用CSRF保護,可以在下面看到相應的XML配置。

<http>
    <!-- ... -->
    <csrf disabled="true"/>
</http>

由於spring security的csrf令牌是存儲再HttpSession中的,因此在動態網頁中,可以很方便的獲取到csrf token信息。

<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
    xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:form="http://www.springframework.org/tags/form" version="2.0">
    <jsp:directive.page language="java" contentType="text/html" />
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
    <!-- ... -->

    <c:url var="logoutUrl" value="/logout"/>
    <form:form action="${logoutUrl}"
        method="post">
    <input type="submit"
        value="Log out" />
    <input type="hidden"
        name="${_csrf.parameterName}"
        value="${_csrf.token}"/>
    </form:form>

    <!-- ... -->
</html>
</jsp:root>

如果使用的是靜態網頁,例如前后端分離、restful接口設計和無狀態session的情況下,或許可以考慮如下設計:

​ 將csef token暴露出去,前端通過ajax請求獲取token,並在請求服務時帶上token。當然,這里暴露出去的token應該是只能在內部暴露,使得靜態頁面得到token后,再將網頁響應給用戶。

獲取token

登錄

靜態網頁獲取csrf token的部分代碼

/**
 * @author 賴柄灃 bingfengdev@aliyun.com
 * @version 1.0
 * @date 2020/8/21 12:29
 */
@RestController
@RequestMapping("/csrf")
public class CsrfTokenCtrl {

    @GetMapping(value = "/getToken")
    public HashMap<String, String> getToken(HttpServletRequest request ){
        /**
         * 在這里可以先過濾掉一些非法的請求,只允許內部靜態網頁服務器請求
         */
        
        CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        HashMap<String, String> map = new HashMap<>();
        map.put("csrf_header",token.getHeaderName());
        map.put("csrf",token.getToken());
        return map;
    }

}

End
為了保證系統安全,犧牲一些設計上的完美,在我看來是很有必要的。相信會有更好的方案!以上只是我個人的一些見解。歡迎大家提出不一樣的想法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM