台灣團隊以《海洋危機》出席全球最大桌遊展

摘錄自2019年11月4日自由時報報導

全球最大桌上遊戲大會「德國埃森桌遊展(SPIEL)」上月下旬開展時,台灣業者獨闖,靠實力獲邀參與官方論壇主講「環境友善」議題,直接正名Taiwan登上官網與活動介紹,不是以China(中國)或Chinese Taipei(中華台北)名稱參與,更透過桌遊,將台灣文化輸出全世界。

獨立遊戲出版業者「綿羊犬百寶箱」公司負責人林啟維表示,SPIEL桌遊展是全球最大,展期是上月24至27日,今年便8月收到該展邀請,希望出席大會在上月26日舉辦的官方論壇活動,主講「環境友善」議題,便將今年開發的《海洋危機》獲台灣逾600間學校使用的經驗,到場分享。

林啟維說,論壇中看見德國遊戲市場與亞洲新興市場的差異,永續話題也在當場彼此交鋒,但結束後卻有不少德國、荷蘭等地學校老師、環境工作者現身,指定購買台灣桌遊,另外過去也開發台灣內容的《史前歷險紀》,不少外國人玩完才發現是台灣史前文化,同樣把台灣文化輸出海外。

林啟維認為,「用熱情、專業將自己手邊的事情做好,當機會來臨,我們就會被看見。」而桌遊設計領域,一直由自由派的創作者們領導著,而台灣在遊戲設計與市場成熟度,也大大領先中國,因此這次論壇無任何政治干預,讓自已不僅代表台灣,也更代表東亞桌遊開發與應用的精神,未來將繼續努力,持續在國際舞台綻放。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

HTML&CSS面試高頻考點(二)

HTML&CSS面試高頻考點(一)    

6. W3C盒模型與怪異盒模型

  • 標準盒模型(W3C標準)
  • 怪異盒模型(IE標準)

怪異盒模型下盒子的大小=width(content + border + padding) + margin,即真實大小

*參考標準模式與兼容模式的區別,兼容模式下為怪異盒模型。

*注意box-sizing可以改變盒模型(box-sizing:border-box即為怪異盒模型)。

7. 水平垂直居中的方法

(1)定寬居中

1. absolute + 負margin

//父元素
position: relative;
//子元素
position: absolute;
top: 50%;
left: 50%;
//margin設置自身一半的距離
margin-top: -50px;
margin-left: -50px;

2. absolute + margin: auto

//父元素
position: relative;
//子元素
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;

 3. absolute + calc

//父元素
position: relative;
//子元素
position: absolute;
//減去自身一半的寬高
top: calc(50% - 50px);
left: calc(50% - 50px);

 *calc() 函數用於動態計算長度值。

 4. min-height: 100vh + flex + margin:auto

main{   min-height: 100vh;
   /* vh相對於視窗的高度,視窗高度是100vh */
  /* “視區”所指為瀏覽器內部的可視區域大小,   不包含任務欄標題欄以及底部工具欄的瀏覽器區域大小。 */   display: flex;
} div{   margin: auto;
}

(2)不定寬居中

1. absolute + transform

//父元素
position: relative;
//子元素
position: absolute;
top:50%;
left:50%;
transform:translate(-50%,-50%);

2. line-height

//父元素 .wp { text-align: center; line-height: 300px;
}
//子元素
.box { display: inline-block; vertical-align: middle; line-height: inherit; text-align: left; }

3. flex布局

display: flex;//flex布局
justify-content: center;//使子項目水平居中
align-items: center;//使子項目垂直居中

4. table-cell布局

因為table-cell相當與表格的td,無法設置寬和高,所以嵌套一層,嵌套一層必須設置display: inline-block

<div class="box">
    <div class="content">
        <div class="inner">
        </div>
    </div>
</div> .box { //只有這裏可以設置寬高 display: table; //這是嵌套的一層,會被table-cell覆蓋 } .content { display: table-cell; vertical-align: middle;//使子元素垂直居中 text-align: center;//使子元素水平居中 } .inner { display: inline-block; //子元素 }

8. BFC

 前文鏈接:點擊這裏

BFC:Block formatting context(塊級格式化上下文),是一個獨立的渲染區域,只有Block-level box參与,與外部區域毫不相干。

  • block-level box:display屬性為block, list-item, table的元素。
  • inline-level box:display屬性為inline, inline-box, inline-table的元素。

(1)BFC的布局規則

  • 內部box在垂直方向一個個放置;
  • 同一個BFC的兩個相鄰box的margin會發生重疊;
  • 每個盒子的margin左邊與包含塊的border左邊相接觸,即使存在浮動也是如此;
  • BFC區域不會和float box重疊;
  • 計算BFC高度時,浮動元素也參与計算。

(2)開啟BFC的方法

  • float的值不是none
  • position的值不是static或relative
  • display的值是inline-block, table-cell, flex, table-caption或inline-flex
  • overflow的值不是visible

(3)BFC的作用

1. 避免margin塌陷

根據BFC的布局規則2,我們可以通過設置兩個不同的BFC的方式解決margin塌陷的問題。

2. 自適應兩欄布局

根據BFC的布局規則3和4,我們將右側div開啟BFC就可以形成自適應兩欄布局。

.left { float: left; //左側浮動 }

.left { float: left;
} .right { overflow: hidden; //開啟BFC }

3. 清除浮動

當不給父節點設置高度的時候,如果子節點設置浮動,父節點會發生高度塌陷。這個時候就要清除浮動。

根據規則5,只需給父元素激活BFC就可以達到目的。

.par { overflow: hidden; //父元素開啟BFC } .child { float: left; //子元素浮動 }

9. 清除浮動

 這篇有寫:點這裏

10. position屬性

 這篇有寫:點這裏

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

SpringSceurity(5)—短信驗證碼登陸功能

SpringSceurity(5)—短信驗證碼登陸功能

有關SpringSceurity系列之前有寫文章

1、SpringSecurity(1)—認證+授權代碼實現

2、SpringSecurity(2)—記住我功能實現

3、SpringSceurity(3)—圖形驗證碼功能實現

4、SpringSceurity(4)—短信驗證碼功能實現

一、短信登錄驗證機制原理分析

了解短信驗證碼的登陸機制之前,我們首先是要了解用戶賬號密碼登陸的機制是如何的,我們來簡要分析一下Spring Security是如何驗證基於用戶名和密碼登錄方式的,

分析完畢之後,再一起思考如何將短信登錄驗證方式集成到Spring Security中。

1、賬號密碼登陸的流程

一般賬號密碼登陸都有附帶 圖形驗證碼記住我功能 ,那麼它的大致流程是這樣的。

1、 用戶在輸入用戶名,賬號、圖片驗證碼後點擊登陸。那麼對於springSceurity首先會進入短信驗證碼Filter,因為在配置的時候會把它配置在
UsernamePasswordAuthenticationFilter之前,把當前的驗證碼的信息跟存在session的圖片驗證碼的驗證碼進行校驗。

2、短信驗證碼通過後,進入 UsernamePasswordAuthenticationFilter 中,根據輸入的用戶名和密碼信息,構造出一個暫時沒有鑒權的
 UsernamePasswordAuthenticationToken,並將 UsernamePasswordAuthenticationToken 交給 AuthenticationManager 處理。

3、AuthenticationManager 本身並不做驗證處理,他通過 for-each 遍歷找到符合當前登錄方式的一個 AuthenticationProvider,並交給它進行驗證處理
,對於用戶名密碼登錄方式,這個 Provider 就是 DaoAuthenticationProvider。

4、在這個 Provider 中進行一系列的驗證處理,如果驗證通過,就會重新構造一個添加了鑒權的 UsernamePasswordAuthenticationToken,並將這個
 token 傳回到 UsernamePasswordAuthenticationFilter 中。

5、在該 Filter 的父類 AbstractAuthenticationProcessingFilter 中,會根據上一步驗證的結果,跳轉到 successHandler 或者是 failureHandler。

流程圖

2、短信驗證碼登陸流程

因為短信登錄的方式並沒有集成到Spring Security中,所以往往還需要我們自己開發短信登錄邏輯,將其集成到Spring Security中,那麼這裏我們就模仿賬號

密碼登陸來實現短信驗證碼登陸。

1、用戶名密碼登錄有個 UsernamePasswordAuthenticationFilter,我們搞一個SmsAuthenticationFilter,代碼粘過來改一改。
2、用戶名密碼登錄需要UsernamePasswordAuthenticationToken,我們搞一個SmsAuthenticationToken,代碼粘過來改一改。
3、用戶名密碼登錄需要DaoAuthenticationProvider,我們模仿它也 implenments AuthenticationProvider,叫做 SmsAuthenticationProvider。

這個圖是網上找到,自己不想畫了

我們自己搞了上面三個類以後,想要實現的效果如上圖所示。當我們使用短信驗證碼登錄的時候:

1、先經過 SmsAuthenticationFilter,構造一個沒有鑒權的 SmsAuthenticationToken,然後交給 AuthenticationManager處理。

2、AuthenticationManager 通過 for-each 挑選出一個合適的 provider 進行處理,當然我們希望這個 provider 要是 SmsAuthenticationProvider。

3、驗證通過後,重新構造一個有鑒權的SmsAuthenticationToken,並返回給SmsAuthenticationFilter。
filter 根據上一步的驗證結果,跳轉到成功或者失敗的處理邏輯。

二、代碼實現

1、SmsAuthenticationToken

首先我們編寫 SmsAuthenticationToken,這裏直接參考 UsernamePasswordAuthenticationToken 源碼,直接粘過來,改一改。

說明

principal 原本代表用戶名,這裏保留,只是代表了手機號碼。
credentials 原本代碼密碼,短信登錄用不到,直接刪掉。
SmsCodeAuthenticationToken() 兩個構造方法一個是構造沒有鑒權的,一個是構造有鑒權的。
剩下的幾個方法去除無用屬性即可。

代碼

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中該字段代表登錄的用戶名,
     * 在這裏就代表登錄的手機號碼
     */
    private final Object principal;

    /**
     * 構建一個沒有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 構建擁有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

2、SmsAuthenticationFilter

然後編寫 SmsAuthenticationFilter,參考 UsernamePasswordAuthenticationFilter 的源碼,直接粘過來,改一改。

說明

原本的靜態字段有 usernamepassword,都幹掉,換成我們的手機號字段。
SmsCodeAuthenticationFilter() 中指定了這個 filter 的攔截 Url,我指定為 post 方式的 /sms/login
剩下來的方法把無效的刪刪改改就好了。

代碼

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /**
     * form表單中手機號碼的字段name
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = "mobile";
    /**
     * 是否僅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        //短信驗證碼的地址為/sms/login 請求也是post
        super(new AntPathRequestMatcher("/sms/login", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public String getMobileParameter() {
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}

3、SmsAuthenticationProvider

這個方法比較重要,這個方法首先能夠在使用短信驗證碼登陸時候被 AuthenticationManager 挑中,其次要在這個類中處理驗證邏輯。

說明

實現 AuthenticationProvider 接口,實現 authenticate() 和 supports() 方法。

代碼

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    /**
     * 處理session工具類
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_SMS";

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();

        checkSmsCode(mobile);

        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        // 此時鑒權成功后,應當重新 new 一個擁有鑒權的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 從session中獲取圖片驗證碼
        SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(new ServletWebRequest(request), SESSION_KEY_PREFIX);
        String inputCode = request.getParameter("smsCode");
        if(smsCodeInSession == null) {
            throw new BadCredentialsException("未檢測到申請驗證碼");
        }

        String mobileSsion = smsCodeInSession.getMobile();
        if(!Objects.equals(mobile,mobileSsion)) {
            throw new BadCredentialsException("手機號碼不正確");
        }

        String codeSsion = smsCodeInSession.getCode();
        if(!Objects.equals(codeSsion,inputCode)) {
            throw new BadCredentialsException("驗證碼錯誤");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判斷 authentication 是不是 SmsCodeAuthenticationToken 的子類或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

4、SmsCodeAuthenticationSecurityConfig

既然自定義了攔截器,可以需要在配置里做改動。

代碼

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private SmsUserService smsUserService;
    @Autowired
    private AuthenctiationSuccessHandler authenctiationSuccessHandler;
    @Autowired
    private AuthenctiationFailHandler authenctiationFailHandler;

    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenctiationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenctiationFailHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        //需要將通過用戶名查詢用戶信息的接口換成通過手機號碼實現
        smsCodeAuthenticationProvider.setUserDetailsService(smsUserService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

5、SmsUserService

因為用戶名,密碼登陸最終是通過用戶名查詢用戶信息,而手機驗證碼登陸是通過手機登陸,所以這裏需要自己再實現一個SmsUserService

@Service
@Slf4j
public class SmsUserService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RolesUserMapper rolesUserMapper;

    @Autowired
    private RolesMapper rolesMapper;

    /**
     * 手機號查詢用戶
     */
    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
        log.info("手機號查詢用戶,手機號碼 = {}",mobile);
        //TODO 這裏我沒有寫通過手機號去查用戶信息的sql,因為一開始我建user表的時候,沒有建mobile字段,現在我也不想臨時加上去
        //TODO 所以這裏暫且寫死用用戶名去查詢用戶信息(理解就好)
        User user = userMapper.findOneByUsername("小小");
        if (user == null) {
            throw new UsernameNotFoundException("未查詢到用戶信息");
        }
        //獲取用戶關聯角色信息 如果為空說明用戶並未關聯角色
        List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId());
        if (CollectionUtils.isEmpty(userList)) {
            return user;
        }
        //獲取角色ID集合
        List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList());
        List<Roles> rolesList = rolesMapper.findByIdIn(ridList);
        //插入用戶角色信息
        user.setRoles(rolesList);
        return user;
    }
}

6、總結

到這裏思路就很清晰了,我這裡在總結下。

1、首先從獲取驗證的時候,就已經把當前驗證碼信息存到session,這個信息包含驗證碼和手機號碼。

2、用戶輸入驗證登陸,這裡是直接寫在SmsAuthenticationFilter中先校驗驗證碼、手機號是否正確,再去查詢用戶信息。我們也可以拆開成用戶名密碼登陸那樣一個
過濾器專門驗證驗證碼和手機號是否正確,正確在走驗證碼登陸過濾器。

3、在SmsAuthenticationFilter流程中也有關鍵的一步,就是用戶名密碼登陸是自定義UserService實現UserDetailsService后,通過用戶名查詢用戶名信息而這裡是
通過手機號查詢用戶信息,所以還需要自定義SmsUserService實現UserDetailsService后。

三、測試

1、獲取驗證碼

獲取驗證碼的手機號是 15612345678 。因為這裏沒有接第三方的短信SDK,只是在後台輸出。

向手機號為:15612345678的用戶發送驗證碼:254792

2、登陸

1)驗證碼輸入不正確

發現登陸失敗,同樣如果手機號碼輸入不對也是登陸失敗

2)登陸成功

當手機號碼 和 短信驗證碼都正確的情況下 ,登陸就成功了。

參考

1、Spring Security技術棧開發企業級認證與授權(JoJo)

2、SpringBoot 集成 Spring Security(8)——短信驗證碼登錄

別人罵我胖,我會生氣,因為我心裏承認了我胖。別人說我矮,我就會覺得好笑,因為我心裏知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(21)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

Spring IoC 循環依賴的處理

前言

本系列全部基於 Spring 5.2.2.BUILD-SNAPSHOT 版本。因為 Spring 整個體系太過於龐大,所以只會進行關鍵部分的源碼解析。

本篇文章主要介紹 Spring IoC 是怎麼解決循環依賴的問題的。

正文

什麼是循環依賴

循環依賴就是循環引用,就是兩個或多個 bean 相互之間的持有對方,比如A引用B,B引用A,像下面偽代碼所示:

public class A {
    private B b;
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    // 省略get和set方法...
}

Spring 如何解決循環依賴

Spring IoC 容器對循環依賴的處理有三種情況:

  1. 構造器循環依賴:此依賴 Spring 無法處理,直接拋出 BeanCurrentlylnCreationException 異常。
  2. 單例作用域下的 setter 循環依賴:此依賴 Spring 通過三級緩存來解決。
  3. 非單例的循環依賴:此依賴 Spring 無法處理,直接拋出 BeanCurrentlylnCreationException 異常。

構造器循環依賴

還是假設上面的A和B類是構造器循環依賴,如下所示:

public class A {
    private B b;
    
    public A(B b) {
        this.b = b;
    }
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    public B(A a) {
        this.a = a;
    }
    
    // 省略get和set方法...
}

然後我們在 XML 中配置了構造器自動注入,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="constructor" />

    <bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="constructor" />

</beans>

那麼我們在獲取 A 時,首先會進入 doGetBean() 方法(該方法在Spring IoC bean 的加載中分析過),會進行到如下代碼塊:

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

    // 省略其它代碼...
    
    // 如果 bean 的作用域是單例
    if (mbd.isSingleton()) {
        // 創建和註冊單例 bean
        sharedInstance = getSingleton(beanName, () -> {
            try {
                // 創建 bean 實例
                return createBean(beanName, mbd, args);
            }
            catch (BeansException ex) {
                destroySingleton(beanName);
                throw ex;
            }
        });
        // 獲取bean實例
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }
    
    // 省略其它代碼...
 
}

上面方法中的 getSingleton() 方法會判斷是否是第一次創建該 bean,如果是第一次會先去創建 bean,也就是調用 ObjectFacotygetObject() 方法,即調用 createBean() 方法創建 bean 前,會先將當前正要創建的 bean 記錄在緩存 singletonsCurrentlyInCreation 中。

在創建A時發現依賴 B,便先去創建 B;B在創建時發現依賴A,此時A因為是通過構造函數創建,所以沒創建完,便又去創建A,發現A存在於 singletonsCurrentlyInCreation,即正在創建中,便拋出 BeanCurrentlylnCreationException 異常。

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    // 加鎖
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        // 一級緩存中不存在當前 bean,也就是當前 bean 第一次創建
        if (singletonObject == null) {
            // 如果當前正在銷毀 singletons,拋出異常
            if (this.singletonsCurrentlyInDestruction) {
                throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)");
            }
            // 創建單例 bean 之前的回調
            beforeSingletonCreation(beanName);
            boolean newSingleton = false;
            boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
            if (recordSuppressedExceptions) {
                this.suppressedExceptions = new LinkedHashSet<>();
            }
            try {
                // 獲取 bean 實例
                singletonObject = singletonFactory.getObject();
                newSingleton = true;
				}
            // 省略異常處理...
            finally {
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = null;
                }
                // 創建單例 bean 之後的回調
                afterSingletonCreation(beanName);
            }
            if (newSingleton) {
                // 將 singletonObject 放入一級緩存,並從二級和三級緩存中移除
                addSingleton(beanName, singletonObject);
            }
        }
        // 返回 bean 實例
        return singletonObject;
    }
}

// 單例 bean 創建前的回調方法,默認實現是將 beanName 加入到當前正在創建 bean 的緩存中,
// 這樣便可以對循環依賴進行檢測
protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

// 單例 bean 創建后的回調方法,默認實現是將 beanName 從當前正在創建 bean 的緩存中移除
protected void afterSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
        throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 這邊bean已經初始化完成了,放入一級緩存
        this.singletonObjects.put(beanName, singletonObject);
        // 移除三級緩存
        this.singletonFactories.remove(beanName);
        // 移除二級緩存
        this.earlySingletonObjects.remove(beanName);
        // 將 beanName 添加到已註冊 bean 緩存中
        this.registeredSingletons.add(beanName);
    }
}

setter循環依賴

還是假設上面的A和B類是 field 屬性依賴注入循環依賴,如下所示:

public class A {
    private B b;
    
    // 省略get和set方法...
}
public class B {
    private A a;
    
    // 省略get和set方法...
}

然後我們在 XML 中配置了按照類型自動注入,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.leisurexi.ioc.circular.reference.A" autowire="byType" />

    <bean id="b" class="com.leisurexi.ioc.circular.reference.B" autowire="byType" />

</beans>

Spring 在解決單例循環依賴時引入了三級緩存,如下所示:

// 一級緩存,存儲已經初始化完成的bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二級緩存,存儲已經實例化完成的bean
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三級緩存,存儲創建bean實例的ObjectFactory
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// 按先後順序記錄已經註冊的單例bean
private final Set<String> registeredSingletons = new LinkedHashSet<>(256);

首先在創建A時,會進入到 doCreateBean() 方法(前面的流程可以查看Spring IoC bean 的創建一文),如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
    // 獲取bean的實例
    BeanWrapper instanceWrapper = null;
    if (instanceWrapper == null) {
        // 通過構造函數反射創建bean的實例,但是屬性並未賦值
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    // 獲取bean的實例
    final Object bean = instanceWrapper.getWrappedInstance();
    
    // 省略其它代碼...

    // bean的作用域是單例 && 允許循環引用 && 當前bean正在創建中
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    // 如果允許bean提前曝光
    if (earlySingletonExposure) {
        // 將beanName和ObjectFactory形成的key-value對放入singletonFactories緩存中
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
    
    // 省略其它代碼...
    
}

在調用 addSingletonFactory() 方法前A的實例已經創建出來了,只是還未進行屬性賦值和初始化階段,接下來將它放入了三級緩存中,如下:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    // 加鎖
    synchronized (this.singletonObjects) {
        // 如果一級緩存中不包含當前bean
        if (!this.singletonObjects.containsKey(beanName)) {
            // 將ObjectFactory放入三級緩存
            this.singletonFactories.put(beanName, singletonFactory);
            // 從二級緩存中移除
            this.earlySingletonObjects.remove(beanName);
            // 將beanName加入到已經註冊過的單例bean緩存中
            this.registeredSingletons.add(beanName);
        }
    }
}

接下來A進行屬性賦值階段(會在後續文章中單獨分析這個階段),發現依賴B,便去獲取B,發現B還沒有被創建,所以走創建流程;在B進入屬性賦值階段時發現依賴A,就去調用 getBean() 方法獲取A,此時會進入 getSingleton() 方法(該方法的調用流程在Spring IoC bean 的加載一文中分析過),如下:

public Object getSingleton(String beanName) {
    // allowEarlyReference設置為true表示允許早期依賴
    return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 先從一級緩存中,檢查單例緩存是否存在
    Object singletonObject = this.singletonObjects.get(beanName);
    // 如果為空,並且當前bean正在創建中,鎖定全局變量進行處理
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // 從二級緩存中獲取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 二級緩存為空 && bean允許提前曝光
            if (singletonObject == null && allowEarlyReference) {
                // 從三級緩存中獲取bean對應的ObjectFactory
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    // 調用預先設定的getObject(),獲取bean實例
                    singletonObject = singletonFactory.getObject();
                    // 放入到二級緩存中,並從三級緩存中刪除
                    // 這時bean已經實例化完但還未初始化完
                    // 在該bean未初始化完時如果有別的bean引用該bean,可以直接從二級緩存中取出返回
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    return singletonObject;
}

嘗試一級緩存 singletonObjects (肯定沒有,因為A還沒初始化完全),嘗試二級緩存 earlySingletonObjects(也沒有),嘗試三級緩存 singletonFactories,由於A通過 ObjectFactory 將自己提前曝光了,所以B能夠通過 ObjectFactory.getObject() 拿到A對象(雖然A還沒有初始化完全,但是總比沒有好呀)。B拿到A后順利創建並初始化完成,調用上面分析過的 addSingleton() 方法將自己放入一級緩存中。此時返回A中,A也能順利拿到完全初始化的B進行後續的階段,最後也將自己放入一級緩存中,並從二級和三級緩存中移除。

過程圖如下所示:

非單例循環依賴

對於非單例的 bean,Spring 容器無法完成依賴注入,因為 Spring 容器不進行緩存,因此無法提前暴露一個創建中的 bean

總結

本文主要介紹了 Spring 對三種循環依賴的處理,其實還有一種字段循環依賴,比如 @Autowired 註解標註的字段,但它和 setter 循環依賴的解決方法一樣,這裏就沒有多說。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

參考

  • 《Spring 源碼深度解析》—— 郝佳
  • https://juejin.im/post/5c98a7b4f265da60ee12e9b2

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

航空業減碳動起來 澳航允2050年前碳排減到零

摘錄自2019年11月11日中央社報導

澳洲航空今(11日)允諾加入英國航空(British Airways)母公司「國際航空集團」(IAG),要在2050年前,把淨碳排降至零。澳航(Qantas)執行長喬伊斯(Alan Joyce)發布聲明說,氣候變遷的憂慮「真實存在」,「我們之所以做這件事,是因為這件事很重要」。

來自環保團體「反抗滅絕」(Extinction Rebellion, XR)與環保小鬥士桑柏格(Greta Thunberg)壓力越來越大之際,國際航空集團上月允諾2050年前把淨碳排減到零,是首家做此承諾的大型航空公司。航空業者整體則已答應要在2050年前把碳排量減至2005年的一半。

澳航表示,希望能把淨碳排控制在2020年水準,並在10年間投資5000萬澳元開發永續能源來減碳,與傳統航空燃料相比,可減少8成的碳排。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

BMW借普天之力 將在北京建200個充電樁

據知情人士透露,BMW汽車目前與中國普天集團旗下的普天新能源合作,2015年將在北京建設200個充電樁,具體合作模式是BMW提供自己的充電樁,由普天新能源為其建設和運營。   BMW生產的200個交流慢充樁已交付給普天新能源北京分公司,這批充電樁主要是為購買BMWi3、i8和華晨BMW之諾的BMW電動汽車客戶在公共領域充電使用,不過由於中德充電介面是統一的,所以其他品牌的電動汽車也可用其充電。   目前,BMW已在上海市區安裝了40多個公共充電裝置,主要分佈在上海各個BMW授權的經銷商處。BMW還與國家電網上海市電力公司及上海世博發展集團合作,在上海世博園區安裝50個公共充電樁。此外,其還與萬科集團達成戰略合作,在全國範圍內的400多個已建及新建社區內,支持業主安裝個人充電設施,並逐步在社區內配套採用國家通用標準的社區公共充電設施。    

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

《HelloGitHub》第 51 期

興趣是最好的老師,HelloGitHub 就是幫你找到興趣!

簡介

分享 GitHub 上有趣、入門級的開源項目。

這是一個面向編程新手熱愛編程對開源社區感興趣 人群的月刊,月刊的內容包括:各種編程語言的項目讓生活變得更美好的工具書籍、學習筆記、教程等,這些開源項目大多都是非常容易上手,而且非常 Cool。主要是希望大家能動手用起來,加入到開源社區中。

  • 會編程的可以貢獻代碼
  • 不會編程的可以反饋使用這些工具中的 Bug
  • 幫着宣傳你覺得優秀的項目
  • Star 項目⭐️

在瀏覽、參与這些項目的過程中,你將學習到更多編程知識提高編程技巧找到編程的樂趣

最後 HelloGitHub 這個項目就誕生了

以下為本期內容|每個月 28 號發布最新一期|點擊查看往期內容

C 項目

1、goaccess:實時 Web 日誌分析工具

2、u6a:函數式編程語言 Unlambda 的一個樸素實現,包含字節碼編譯器和解釋器。此項目可以幫助初學者理解函數式編程的思想,並提供了實現函數式編程語言解釋器的一些樸素思路。

  • 性能優異:運行性能遠高於官方實現,且優於多數現有的開源實現
  • 穩定可靠:有豐富的測試樣例支撐,可靠性高
  • 簡單樸素:代碼簡單易讀,且提供了實現思路文檔,對初學或者完全沒有學過編譯原理的新手非常友好

C# 項目

3、Netch:一款 Windows 平台的開源遊戲加速工具

4、ScheduleMasterCore:一款基於 .NET Core 開發的分佈式任務調度系統。支持豐富的調度類型、靈活可控的系統參數、簡易的 UI 操作、支持多節點高可用、業務 API 集成等等特性。同時支持多樣化的部署方式,容易上手

5、HandyControl:一套 WPF 控件庫。它幾乎重寫了所有原生樣式,同時包含 70 餘款自定義控件。支持跨平台、國際化,適用於 MVVM 架構開發,扁平化設計、支持動態更換主題和背景色。豐富的自定義控件解決了 View 設計的痛點,讓程序員更加專註於業務邏輯的開發

C++ 項目

6、CnC_Remastered_Collection:EA 發布的《紅警》和《泰伯利亞黎明》遊戲源代碼

7、chinessChess:基於 Qt5 開發的中國象棋網絡對戰平台,支持單機和網絡對戰

Go 項目

8、grmon:Goroutine 的命令行監控工具

9、HackChrome:Go 語言實現的從 Chrome 中獲取自動保存的用戶名密碼工具。目前僅支持 Windows Chrome 中存儲的密碼,但是很有意思還可以學習怎麼用 Go 調用 DLL 動態鏈接庫的姿勢

10、seaweedfs:一款基於 Go 開發的部署方便、使用簡單且強大的分佈式文件系統

11、fate:起中文名工具,去吧!算名先生

Java 項目

12、JApiDocs:一個無需額外註解、開箱即用的 SpringBoot 接口文檔生成工具。特性:

  • 代碼即文檔
  • 支持導出 HTML
  • 同步導出客戶端 Model 代碼
  • 等等

13、PowerJob:基於 Akka 架構的新一代分佈式任務調度與計算框架。支持 CRON、API、固定頻率、固定延遲等調度策略,支持單機、廣播、MapReduce 等多種執行模式,支持在線任務治理與運維,提供 Shell、Python、Java 等功能豐富的任務處理器,提供工作流來編排任務解決依賴關係,使用簡單,功能強大,文檔齊全。同類產品對比:

JavaScript 項目

14、react-trello:任務狀態管理面板組件。實現了拖拽方式管理任務狀態,點擊即可編輯任務內容

15、perfume.js:用於測量第一個 dom 生成的時間、用戶最早可操作時間和組件的生命周期性能的庫。示例代碼:

perfume.start('fibonacci');
fibonacci(400);
perfume.end('fibonacci');
// Perfume.js: fibonacci 0.14 ms

16、Mongood:MongoDB 圖形化的管理工具。特性:

  • 基於微軟 Fluent UI,支持自動黑暗模式
  • 支持完整的 Mongo-shell 數據類型和查詢語法,利用索引實現的自動查詢和排序
  • 支持 Json 數據庫模式,既可用於 Server 也可用於 Client

17、TimeCat:一款 JS 的網頁錄屏工具。參考了遊戲錄像的原理而實現的渲染引擎,生成的錄像文件只有傳統視頻的百分之一!還可以在錄製語音的同時自動生成字幕,導出的視頻文件可以跨端播放。目前已經開發一段時間,後續還將實現更多有意思的功能,歡迎持續關注。在線預覽

18、react-visual-editor:基於 React 組件的可視化拖拽、搭建頁面的代碼生成工具。所見即所得,可以完美還原 UI 設計搞,並支持多款型號手機(可配置)和 PC 效果展示,模板功能可以使你分享你的頁面或者頁面中局部任何部分組件組合,減少相似頁面的重複操作。效果如下:

19、elevator.js:一個 back to top 返回頂部的插件。如他的名字一樣,網頁在返回頂部過程中像電梯向上運行,當頁面返回到頂部時,會有電梯“到達”的提示音。叮~頁面已到達頂部

PHP 項目

20、code6:一款 GitHub 代碼泄露監控系統,通過定期掃描 GitHub 發現代碼泄露行為。特性:

  • 全可視化界面,操作部署簡單
  • 支持 GitHub 令牌管理及智能調度
  • 掃描結果信息豐富,支持批量操作
  • 任務配置靈活,可單獨配置任務掃描參數
  • 支持白名單模式,主動忽略白名單倉庫

Python 項目

21、rich:一個讓你的終端輸出變得“花里胡哨”的三方庫。我的一位前輩告訴我,不要整那些花里胡哨的主題和樣式,這是在自尋煩惱。可是臣妾做不到啊,這麼好看的終端輸出,讓我的心情都愉悅起來了。瞧那性感的語法高亮、整齊的表格、舒服的顏色、進度條等,一切都是值得的

22、poetry:Python 虛擬環境、依賴管理工具。依賴管理工具有很多,我相上了它有三點:通過單文件 pyproject.toml 便可輕鬆的區別安裝、管理開發和正式環境、有版本鎖定可方便回滾、輸出界面簡單清爽。當然它還是個“新生兒”,嘗鮮的風險還是有的,選擇須謹慎

23、free-python-games:真入門級的 Python 遊戲集合庫。都是簡單的小遊戲:貪吃蛇、迷宮、Pong、猜字等,運行方便、代碼簡單易懂。用遊戲開啟的你 Python 學習之旅,玩完再學源碼,其樂無窮啊。安裝運行:

pip install freegames
python -m freegames.snake # freegames.遊戲名

24、py2sec:一款輕量級跨平台 Python “加密”、加速的腳本工具。原理是基於 Cython 將 .py 編譯成 run-time libraries 文件:.so(Linux && Mac)或 .pyd(Win),一定程度上實現了“加密”保護源代碼的功能。參數詳解如下:

-v,  --version    显示 py2sec 版本
-h,  --help       显示幫助菜單
-p,  --pyth       Python 的版本,默認為你的 Python 命令綁定的 Python 版本
-d,  --directory  Python 項目路徑(如果使用 -d 參數,將編譯整個 Python 項目)
-f,  --file       Python文件(如果使用 -f,將編譯單個 Python 文件)
-m,  --maintain   標記你不想編譯的文件或文件夾路徑
-x  --nthread     編譯啟用的線程數
-q  --quiet       靜默模式,默認 False
-r  --release     Release 模式,清除所有中間文件,只保留加密結果文件,默認 False
python py2sec.py -f test.py
python py2sec.py -f example/test1.py -r
python py2sec.py -d example/ -m test1.py,bbb/

25、oxfs:一個基於 sftp 協議的 fuse 網絡文件系統,功能上類似於 sshfs。特性:

  • 引入了異步併發讀遠端文件機制,提高了文件首次讀速度。
  • 緩存持久化到本地磁盤,下次掛載時訪問更加快速。
  • 異步任務負責同步文件,避免低速的網絡讀寫阻塞上層應用。

Swift 項目

26、Aerial:炫酷的蘋果系統屏保項目。該屏保視頻取材自蘋果零售店 Apple TV 的專用屏保,航拍質量超棒,快換上試試吧。直接下載 Aerial.saver.zip 文件,解壓后雙擊文件“即可食用”

其它

27、shan-shui-inf:自動生成一副山水畫

28、kuboard-press:一款基於 Kubernetes 的微服務管理界面。包含文檔、教程、管理界面和實戰分享

29、vscode-rainbow-fart:一款在你編程時花式誇你的 VSCode 擴展插件。可以根據代碼關鍵字,播放貼近代碼意義的真人語音,並且有一個醒目的項目名字“彩虹屁”

30、flink-training-course:Flink 視頻直播教程回放集合

31、raft-zh_cn:《分佈式 Raft 一致性算法論文》中文翻譯

32、GitHub-Chinese-Top-Charts:每周更新一次的 GitHub 中文項目排行榜

開源書籍

33、go-ast-book:《Go語法樹入門:開啟自製編程語言和編譯器之旅》

機器學習

34、Surprise:一款簡單易用基於 Python scikit 的推薦系統。如果你想用 Python 上手做一套推薦系統,那你可以試試它

35、djl:亞馬遜開源的一款基於 Java 語言的深度學習框架。對於 Java 開發者而言,可以在 Java 中開發及應用原生的機器學習和深度學習模型,同時簡化了深度學習開發的難度。通過 DJL 提供直觀的、高級的 API,Java 開發人員可以訓練自己的模型,或者利用數據科學家用 Python 預先訓練好的模型來進行推理。如果您恰好是對學習深度學習感興趣的 Java 開發者,那麼這個項目完全對口。運行效果如下:

36、data-science-ipython-notebooks:數據科學的 IPython 集合。包含:TensorFlow、Theano、Caffe、scikit-learn、Spark、Hadoop、MapReduce、matplotlib、pandas、SciPy 等方方面面

最後

如果你發現了 GitHub 上有趣的項目,歡迎在 HelloGitHub 項目提 issues 告訴我們。

關注 HelloGitHub 公眾號獲取第一手的更新

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

與特斯拉 Mddel X 比拼 奧迪 2018 年左右將推純電動車

國外媒體美國汽車新聞網報導,奧迪將推出全新的純電動 SUV 車型與特斯拉 Model X 車型競爭,但奧迪認為插電混合動力車型才是目前新能源汽車市場最理想的車型。   奧迪將在 2018 年左右發布一款續航里程可達到 498 公里的純電動 SUV,將採用全新的造型設計理念,基於第二代 MLB 平台研發,並藉鑑新一代奧迪 Q5 車型的部分技術與設計。奧迪全新的純電動 SUV 車型將可在 20 分鐘內完成 80% 的充電,續航里程超過特斯拉 Model X 車型,對其構成不小的威脅。   不過,奧迪執行長斯泰德(Rupert Stadler)近日表示在未來的 10 至 15 年內,插電混合動力汽車將是消費者選擇新能源汽車時的首選車型。插電混合動力車型使用一台汽油引擎或柴油引擎和一台電動機聯合驅動,有一定的純電動續航里程,十分適合消費者在市區中駕駛。但是斯泰德同時表示隨著充電網絡建設的繼續,消費者未來對電動車的接受程度將增加。      

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

(七) SpringBoot起飛之路-整合SpringSecurity(Mybatis、JDBC、內存)

興趣的朋友可以去了解一下前五篇,你的贊就是對我最大的支持,感謝大家!

(一) SpringBoot起飛之路-HelloWorld

(二) SpringBoot起飛之路-入門原理分析

(三) SpringBoot起飛之路-YAML配置小結(入門必知必會)

(四) SpringBoot起飛之路-靜態資源處理

(五) SpringBoot起飛之路-Thymeleaf模板引擎

(六) SpringBoot起飛之路-整合JdbcTemplate-Druid-MyBatis

說明:

  • 這一篇的目的還是整合,也就是一個具體的實操體驗,原理性的沒涉及到,我本身也沒有深入研究過,就不獻醜了

  • SpringBoot 起飛之路 系列文章的源碼,均同步上傳到 github 了,有需要的小夥伴,隨意去 down

    • https://github.com/ideal-20/Springboot-Study-Code
  • 才疏學淺,就會點淺薄的知識,大家權當一篇工具文來看啦,不喜勿憤哈 ~

(一) 初識 Spring Security

(1) 引言

權限以及安全問題,雖然並不是一個影響到程序、項目運行的必須條件,但是卻是開發中的一項重要考慮因素,例如某些資源我們不想被訪問到或者我們某些方法想要滿足指定身份才可以訪問,我們可以使用 AOP 或者過濾器來實現要求,但是實際上,如果代碼涉及的邏輯比較多以後,代碼是極其繁瑣,冗餘的,而有很多開發框架,例如 Spring Security,Shiro,已經為我們提供了這種功能,我們只需要知道如何正確配置以及使用它了

(2) 基本介紹

先看一下官網的介紹

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一個功能強大且高度可定製的身份驗證和訪問控制框架。它是保護基於spring的應用程序的實際標準。

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security是一個框架,側重於為Java應用程序提供身份驗證和授權。與所有Spring項目一樣,Spring安全性的真正強大之處在於它很容易擴展以滿足定製需求

簡單的說,Spring Security 就是一個控制訪問權限,強大且完善的框架

Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分,同時它們也是 Spring Security 提供的核心功能

用戶認證:用戶認證就是指這個用戶身份是否合法,一般我們的用戶認證就是通過校驗用戶名密碼,來判斷用戶身份的合法性,確定身份合法后,用戶就可以訪問該系統

用戶授權:如果不同的用戶需要有不同等級的權限,就涉及到用戶授權,用戶授權就是對用戶能訪問的資源,所能執行的操作進行控制,根據不同用戶角色來劃分不同的權限

(二) 靜態頁面導入 And 環境搭建

(1) 關於靜態頁面

A:頁面介紹

頁面是我自己臨時弄得,有需要的朋友可以去我 GitHub:ideal-20 下載源碼,簡單說明一下這個頁面

做一個靜態頁面如果嫌麻煩,也可以單純的自己創建一些簡單的頁面,寫幾個標題文字,能體現出當前是哪個頁面就好了

我代碼中用的這些頁面,就是拿開源的前端組件框架進行了一點的美化,然後方便講解一些功能,頁面模板主要是配合 Thymeleaf

1、目錄結構

├── index.html                        // 首頁
├── images                            // 首頁圖片,僅美觀,無實際作用
├── css                               // 上線項目文件,放在服務器即可正常訪問
├── js                                // 項目截圖
├── views                             // 總子頁面文件夾,權限驗證的關鍵頁面
│   ├── login.html					  // 自製登錄頁面(用來替代 Spring Security 默認的 )
│   ├── L-A							  // L-A 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-B							  // L-B 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-C							  // L-C 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html

B:導入到項目

主要就是把基本一些鏈接,引入什麼的先替換成 Thymeleaf 的標籤格式,這裏語法用的不是特別多,即使對於 Thymeleaf 不是很熟悉也是很容易看懂的,當然如果仍然感覺有點吃力,可以單純的做成 html,將就一下,或者去看一下我以前的文章哈,裏面有關於 Thymeleaf 入門的講解

css、image、js 放到 resources –> static 下 ,views 和 index.html 放到 resources –> templates下

(2) 環境搭建

A:引入依賴

這一部分引入也好,初始化項目的時候,勾選好自動生成也好,只要依賴正常導入了即可

  • 引入 Spring Security 模塊
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

關鍵的依賴主要就是上面這個啟動器,但是還有一些就是常規或者補充的了,例如 web、thymeleaf、devtools

thymeleaf-extras-springsecurity5 這個後面講解中會提到,是用來配合 Thymeleaf 整合 Spring Security 的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

B:頁面跳轉 Controller

因為我們用了模板,頁面的跳轉就需要交給 Controller 了,很簡單,首先是首頁的,當然關於頁面這個就無所謂了,我隨便跳轉到了我的博客,接着還有一個登錄頁面的跳轉

有一個小 Tip 需要提一下,因為 L-A、L-B、L-C 文件夾下都有3個頁面 a.html 、b.html 、c.html,所以可以利用 @PathVariable 寫一個較為通用的跳轉方法

@Controller
public class PageController {

    @RequestMapping({"/", "index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/about")
    public String toAboutPage() {
        return "redirect:http://www.ideal-20.cn";
    }

    @RequestMapping("/toLoginPage")
    public String toLoginPage() {
        return "views/login";
    }

    @RequestMapping("/levelA/{name}")
    public String toLevelAPage(@PathVariable("name") String name) {
        return "views/L-A/" + name;
    }

    @RequestMapping("/levelB/{name}")
    public String toLevelBPage(@PathVariable("name") String name) {
        return "views/L-B/" + name;
    }

    @RequestMapping("/levelC/{name}")
    public String toLevelCPage(@PathVariable("name") String name) {
        return "views/L-C/" + name;
    }
}

C:環境搭建最終效果

  • 為了貼圖方便,我把頁面拉窄了一點
  • 首頁右上角應該為登錄的鏈接,這裡是因為,我運行的是已經寫好的代碼,不登錄頁面例如 L-A-a 等模塊就显示不出來,所以拿一個定義好的管理員身份登陸了
  • 關於如何使其自動切換显示登陸還是登錄后信息,在後面會講解

1、首頁

2、子頁面

L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是一樣的,只是文字有一點變化

3、登陸頁面

(三) 整合 Spring Security (內存中)

這一部分,為了簡化一些,容易理解一些,沒有從帶數據的場景出發(因為涉及代碼少一些,所以講解會多一點),而是直接將一些身份等等寫死了,寫到了內存中,方便理解,接着會在下一個標題中給出含有數據庫的寫法(講解會少一些,重點只說一些與前一種的不同點)

(1) 配置授權內容

A:源碼了解用戶授權方式

可以去官網看一下,官網有提供給我們一些樣例,其中有一個關於配置類的小樣例,也就是下面這個,我們通過這個例子,展開分析

https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#jc-custom-dsls

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .apply(customDsl())
                .flag(true)
                .and()
            ...;
    }
}

1、創建 config –> SecurityConfig 配置類

  • 創建一個配置類,像官網中一樣,繼承 WebSecurityConfigurerAdapter
  • 類上添加 @EnableWebSecurity 註解,代表開啟WebSecurity模式
  • 重寫 configure(HttpSecurity http) 方法
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

既然是重寫,那麼我們可以點進去,看一下父類中關於 configure(HttpSecurity http) 方法的源碼註釋,它有很多有用的信息

我摘選出這麼兩小段,第一段的意思就是 ,我們想要使用 HttpSecurity ,要通過重寫,不能通過 super 調用,否則會有覆蓋問題,第二段就是給出了一個默認的配置方式

* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();

2、按照源碼的註釋分析

我們先按照剛才看到的註釋寫出來,首先能看到,它是支持一個鏈式調用的

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and().formLogin()
            .and().httpBasic();
}
  • 通過字面意思也很好理解,authorizeRequests 是關於請求授權的,所以要涉及到關於請求授權(允許指定身份用戶訪問不同權限的資源)的問題就需要調用了

  • 其次,anyRequest().authenticated() 也就是說所有HTTP請求都需要被認證

  • 接着看,通過 and() 連接了一些新的內容,例如選擇表單登錄還是 HTTPBasic 的方式(這裏認證的過程就是讓你輸入用戶名密碼,檢測你的身份,兩種方式表單或者那種彈窗)

Basic認證是一種較為簡單的HTTP認證方式,客戶端通過明文(Base64編碼格式)傳輸用戶名和密碼到服務端進行認證,通常需要配合HTTPS來保證信息傳輸的安全

給大家演示一下:

  • 如果不指定一種認證方式 .and().formLogin() 或者 .and().httpBasic() 訪問任何頁面都會提示 403 禁止訪問的錯誤
  • 指定 .and().formLogin() 認證,彈出一個表單頁面(自帶的,和自己創建的沒關係)
  • 指定 .and().httpBasic(); 認證,彈出一個窗口進行 HTTPBasic 認證

B:自定製用戶授權

1、先看源碼註釋

默認配置,設定了所有 HTTP 請求 都需要進行認證,所以我們在訪問首頁等的時候也會被攔截,但是實際情況下,有一些頁面是可以被任何人訪問的,例如首頁,或者自定義的登陸的等頁面,這時候需要用自己定義一些用戶授權的規則

在 WebSecurityConfigurerAdapter 的 formLogin() 註釋附近,又看到了一個有意思的內容

注:&quot 代表引號

* 		http
* 			.authorizeRequests(authorizeRequests ->
* 				authorizeRequests
* 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
* 			)

這就是我們想要找的,自定義的配置,通過一個一個 antMatchers 進行匹配,通過 hasRole 來規定其合法的身份,也就是說只有滿足這個身份的用戶才能訪問前面規定的路徑資源

Matchers 前面的 ant 前綴代表着,他可以用 ant 風格的路徑表達式(舉例的時候就能看懂了)

通配符 說明
? 匹配任何單字符
* 匹配0或者任意數量的字符
** 匹配0或者更多的目錄

補充: 如果想用正則表達式的方式,可以用這個方法 .regexMatchers()

當然了,有很多情況下,你想要讓任何人都可以訪問某個路徑,例如首頁,permitAll() 方法 就可以達到這種效果,在這裏補充一些常用的方法

  • permitAll() :允許任何訪問

  • denyAll():拒絕所有訪問

  • anonymous():允許匿名用戶訪問

  • authenticated() :允許認證的用戶進行訪問

  • hasRole(String) :如果用戶具備給定角色(用戶組)的話,就允許訪問/

  • hasAnyRole(String…) :如果用戶具有給定角色(用戶組)中的一個的話,允許訪問.

  • rememberMe() :如果用戶是通過Remember-me功能認證的,就允許訪問

  • fullyAuthenticated():如果用戶是完整認證的話(不是通過Remember-me功能認證的),就允許訪問

  • hasAuthority(String):如果用戶具備給定權限的話就允許訪問

  • hasAnyAuthority(String…) :如果用戶具備給定權限中的某一個的話,就允許訪問

  • hasIpAddress(String) :如果請求來自給定ip地址的話,就允許訪問.

  • not() :對其他訪問結果求反

說明:hasAnyAuthority(“ROLE_ADMIN”) 和 hasRole(“ADMIN”) 的區別就是,後者會自動使用 它會自動使用 “ROLE_” 前綴

2、我們來定製一下用戶授權

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
        	.antMatchers("/").permitAll()
        	.antMatchers("/levelA/**").hasRole("vip1")
        	.antMatchers("/levelB/**").hasRole("vip2")
        	.antMatchers("/levelC/**").hasRole("vip3")
        	.and().formLogin();
}

我們上面代碼的意思就是,當訪問 /levelA/ /levelB/ /levelC/ 這三個路徑下面的任意文件(這裡有 a/b/c.html)都需要認證,身份分別是對應 vip1、vip2、vip3,而其他頁面,就可以隨便訪問了

很顯然,雖然說規定了授權的內容,也就是哪些權限的用戶,可以訪問哪些資源,但是我們由於並沒有配置用戶的信息(合法的或者非法的),所以自然,前面的登錄頁面,都是會直接報錯的,下面我們來分析一下,如何進行認證

(2) 配置認證內容

A:源碼了解用戶認證方式

剛才的授權部分,我們重寫了 configure(HttpSecurity http) 方法,我們繼續看看重寫方法中,有沒有可能幫助我們驗證身份,進行用戶認證的方法,我們首先來看這個方法 configure(AuthenticationManagerBuilder auth)

先去看一下源碼的註釋(此部分的格式,我稍微修改了一下,方便觀看):

這是其中他局舉的一個例子,其實這個就是我們想要的,看註釋也可以看出來,他就是用來在內存中啟用基於用戶名的身份驗證的

* protected void configure(AuthenticationManagerBuilder auth) {
*  auth
*  // enable in memory based authentication with a user named
*  // &quot;user&quot; and &quot;admin&quot;
*  		.inMemoryAuthentication()
*   		.withUser(&quot;user&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;).and()
*        	.withUser(&quot;admin&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;, &quot;ADMIN&quot;);
* }

照貓畫虎,我們也先這麼做

B:自定製用戶認證

代碼如下:

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("admin")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2", "vip3")
            .and()
            .withUser("ideal-20")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2")
            .and()
            .withUser("jack")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1");
}

我們就是照着例子打的,但是,其中我們又加入了編碼的問題,它要求必須進行編碼,否則會報錯,官方推薦的是bcrypt加密方式,我們這裏就用這種,當然自己用常見的 MD5 等等都是可以的,可以自己寫一個工具類

到這裏,測試一下,實際上就可以按照身份的不同,從而擁有訪問不同路徑資源你的權限了,主要的功能已經實現了,下面補充一些,更加友好的功能,例如登錄註銷按鈕的显示,以及記住密碼等等

(3) 註銷問題

1、註銷配置

當然了,前面因為已經有很多配置了,所以可以通過 .and() 進行連接,例如 .and().xxx,或者像下面給出的,單獨再寫一個 http.xxx

@Override
protected void configure(HttpSecurity http) throws Exception {
   ......
    // 註銷配置
	http.logout().logoutSuccessUrl("/")
}

上面短短一句的代碼, logout() 代表開啟了註銷的配置,logoutSuccessUrl(“/”),代表註銷成功后,返回的頁面,我們令其註銷后回到首頁

前台的頁面中,我已經給出了註銷的按鈕的代碼,當然這不是固定的,不同的 ui 框架,不同的模板引擎都是不一樣的,但是路徑是 /logout

<a class="item" th:href="@{/logout}">
  <i class="address card icon"></i> 註銷
</a>

(4) 根據身份權限显示組件

A:登錄、註銷的显示

還有這樣一種問題,右上角,未登錄的時候,應該显示登陸按鈕,登錄后,應該显示用戶信息,以及註銷等等,這一部分,主要是頁面這邊的問題

显示的條件其實很簡單,就是判斷是否認證了,認證了就取出一些值,沒認證就显示登陸

1、這時,我們就需要引入一個 Thymeleaf 配合 Spring Security 的一個依賴 (當然了如果是別的技術,就不一樣了)

地址如下:

https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

2、導入命名空間

引入這個文件的目的,就是為了在頁面寫權限判斷等相關的內容的時候可以有提示

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

3、修改導航欄邏輯

<!--登錄註銷-->
<div class="right menu">

  <!--如果未登錄-->
  <div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLoginPage}">
      <i class="address card icon"></i> 登錄
    </a>
  </div>

  <!--如果已登錄-->
  <div sec:authorize="isAuthenticated()">
    <a class="item">
      <i class="address card icon"></i>
      用戶名:<span sec:authentication="principal.username"></span>
      <!--角色:<span sec:authentication="principal.authorities"></span>-->
    </a>
  </div>

  <div sec:authorize="isAuthenticated()">
    <a class="item" th:href="@{/logout}">
      <i class="address card icon"></i> 註銷
    </a>
  </div>
</div>

B:組件面板的显示

上面的代碼,解決了導航欄的問題,但是例如我們首頁中,一些板塊,對於不同的用戶的显示也是不同的嗎

正如上面的例子,沒有登錄的用戶,是不能訪問了 /levelA/、 /levelB/、 /levelC/ 下面的任何文件的,只有登錄的用戶,根據權限的大小,才能訪問某一個,或者所有

而我們首頁部分的三個面板就是用來显示這三塊的鏈接,對於沒有足夠身份的人,實際上显示這個面板就已經是多餘了,當然,你可以選擇显示,但是如果想要根據身份显示面板怎麼做呢?

關鍵就是在 div 中添加了這樣一句權限的代碼,沒有這個指定的身份,這個面板就不會显示sec:authorize="hasRole('vip1')"

<div class="column" sec:authorize="hasRole('vip1')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelA/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/b}">L-A-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/c}">L-A-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelB/a}">L-B-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/b}">L-B-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/c}">L-B-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelC/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/b}">L-C-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/c}">L-C-c</a>
    </div>
  </div>
</div>

演示一下:

(5) 記住用戶

如果重啟瀏覽器后,就需要重新登錄,對於一部分用戶來說,他們認為是麻煩的,所以很多網站登錄時都提供記住用戶這種選項

1、一個簡單的配置就可以達到目的,這種情況下,默認的登陸頁面,就會多出一個記住用戶的單選框

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//記住用戶
    http.rememberMe();
}

2、但是如果,登陸頁面是自定義(下面講)的怎麼辦呢?,其實只要修改為如下配置即可,

//定製記住我的參數!
http.rememberMe().rememberMeParameter("remember");

上面的 remember 對應 input 中的 name 屬性值

<input type="checkbox" name="remember"/>
<label>記住密碼</label>

3、它做了哪些事情呢?

可以打開頁面的控制台看一下,實際上配置后,用戶選擇記住密碼后,會自動幫我們增加一個 cookie 叫做 remember-me,過期時間為 14 天,當註銷的時候,這個 cookie 就會被刪除了

(6) 定製登錄頁面

1、配置

自帶的登陸頁面確實,還是比較丑的,版本更低一些的,更是不美觀,如果想要使用自己定製的登陸頁面,可以加入下面的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	// 登陸表單提交請求
    http.formLogin()
	.usernameParameter("username")
	.passwordParameter("password")
	.loginPage("/toLoginPage")
	.loginProcessingUrl("/login")
}
  • .loginPage("/toLoginPage") 就是說,當你訪問一些需要用戶權限認證的頁面時,就會發起這個請求,到你的登錄頁面
  • .loginProcessingUrl("/login") 就是表單中,真正要提交請求的一個路徑
  • 其餘兩個就是關於用戶名和密碼的一個獲取,其值和頁面中用戶名密碼的 name 屬性值一致

2、頁面跳轉

前面我們就提過這個,回顧一下

@RequestMapping("/toLoginPage")
public String toLoginPage() {
    return "views/login";
}

3、自定義登錄頁面的表單提交 action 設置

<form id="login" class="ui fluid form segment" th:action="@{/login}" method="post">
	......
</form>

(7) 關閉csrf

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
	http.csrf().disable();
}

(四) 整合 Spring Security (JDBC)

因為配置內存中的用戶還是相對簡單一些的,所以一些細節也都說了一下,基於上面的基礎,來看一下 如何用 JDBC 實現上面的功能,當然了這部分只能算補充,基本不會這麼用的,下面的整合 MyBatis 才是常用的()

(1) 創建表以及數據

這裏創建了三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(2) 創建實體

我使用了 lombok,不過自己寫 get set 構造方法 也是一樣的

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer uid;
    private String username;
    private String password;
    private String roles;
}

(3) 配置授權內容

這部分沒什麼區別

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //註銷
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
            .and().csrf().disable();
}

(4) 配置認證內容

A:配置數據庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

server:
  port: 8082

B:具體配置

以幾個注意的地方:

  • 查詢語句都是通過 username 查詢

  • usersByUsernameQuery()方法里的參數一定要有一個 true 的查詢結果,所以我直接在查詢語句中寫了一個 true

  • MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候簡單點

  • DataSource dataSource 要在前面注入進來(選擇 sql 的)

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("select username,password,true from user where username = ?")
            .authoritiesByUsernameQuery("select username,roles from user where username = ?")
            .passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence rawPassword) {
                    return MD5Util.MD5EncodeUtf8((String) rawPassword);
                }

                @Override
                public boolean matches(CharSequence rawPassword, String encodedPassword) {
                    return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
                }
            });
}

C:MD5工具類

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

D:修改頁面

到這裏,JDBC 的整合方式就成功了,至於前面的頁面只需要根據我們自己設計的權限進行修改,別的地方和前面內存中的方式是一樣的

<div class="ui stackable three column grid">
  <div class="column" sec:authorize="hasAnyRole('USER','ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelA/a}">L-A-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/b}">L-A-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/c}">L-A-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasAnyRole('ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelB/a}">L-B-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/b}">L-B-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/c}">L-B-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasRole('SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelC/a}">L-C-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/b}">L-C-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/c}">L-C-c</a>
      </div>
    </div>
  </div>
  <!-- <div class="column"></div> -->
</div>

(五) 整合 Spring Security (MyBatis)

因為這部分內容是比較常用的,所以,我盡可能給的完善一些

(1) 添加依賴

像 lombok、commons-lang3 都不是必須的,都是可以使用原生的一些手段替代的,寫到那裡我會提的

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

(2) 創建表

和 JDBC 部分用同樣的表

三個字段,用戶名,密碼,還有角色,插入數據的時候密碼是使用了 md5 加密(自己寫了一個工具類)

這裏更合理了一些,我把權限定義為了普通用戶、普通管理員、超級管理員(自己設計都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用戶名',
  `password` varchar(255) DEFAULT NULL COMMENT '密碼',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');

(3) 整合 MyBatis

在進行 Spring Security 的配置前,最好先把 MyBatis 先整合好,這樣等會只考慮 Spring Security 的問題就可以了

說明:這部分我盡可能簡化了,例如連接池就用默認的,如果這部分感覺還是有點問題,可以參考一下我前幾篇,關於整合 MyBatis 的文章

A:配置數據庫

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: cn.ideal.pojo

server:
  port: 8081

B:配置 Mapper 以及 XML

UserMapper

@Mapper
public interface UserMapper {
    User queryUserByUserName(String username);
}

mapper/UserMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.ideal.mapper.UserMapper">
    <select id="queryUserByUserName" parameterType="String" resultType="cn.ideal.pojo.User">
         select * from user where username = #{username}
    </select>
</mapper>

這裏就不演示測試了,是沒有問題的

(4) 配置授權內容

這部分沒什麼好說的,和前面的都一樣,解釋在內存中配置用戶時已經詳細說過了

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陸表單提交請求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //註銷
            .and().logout().logoutSuccessUrl("/")
            //記住我
            .and().rememberMe().rememberMeParameter("remember")
            //關閉csrf功能:跨站請求偽造,默認只能通過post方式提交logout請求
            .and().csrf().disable();
}

(5) 配置認證內容

A:創建 UserService

創建一個類,實現 UserDetailsService,其實主要就是為了 loadUserByname 方法,在這個類中,我們可以注入 mapper 等等,去查用戶,如果查不到,就還留在這個頁面,如果查到了,做出一定邏輯后(例如判空等等),就會把用戶信息封裝到 Spring Security 自己的的 User類中去,Spring Security 拿前台的數據和它比較,做出操作,例如認證成功或者錯誤

注意:

  • StringUtils 是 commons.lang3 下的,使用需要導包,我們用了一個判空功能,不想用的話,用原生的是一個道理,這不是重點
  • 注意區分自己的 User 和 Spring Security 的 User
@Service
public class UserService<T extends User> implements UserDetailsService{

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.queryUserByUserName(username);
        if (username == null){
            throw  new UsernameNotFoundException("用戶名不存在");
        }

        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        String role = user.getRoles();
        if (StringUtils.isNotBlank(role)){
            authorityList.add(new SimpleGrantedAuthority(role.trim()));
        }
        return new org.springframework.security.core.userdetails
            .User(user.getUsername(),user.getPassword(),authorityList);
    }
}

B:修改配置類

這裏也很熟悉,我們調用就可以調用 userDetailsService 了,同樣還需要指定編碼相關的內容 實例化 PasswordEncoder,就需要重寫 encode、 matches

//定義認證規則
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
        @Override
        public String encode(CharSequence rawPassword) {
            return MD5Util.MD5EncodeUtf8((String) rawPassword);
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
        }
    });
}

C:MD5 工具類補充

其實上面已經給出了,但是怕大家看起來不方便,這裏再貼一下

MD5 工具類,是我以前一個項目中整理的,加鹽的部分,我給註釋掉了,因為我測試的時候可以簡單點

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具類
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大寫MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

香港環保署:去年及今年首9月 空氣中二噁英濃度沒異常

摘錄自2019年11月17日香港電台網站、頭條日報報導

港警近月大量使用催淚彈,其中催淚彈不時出現大量火光,有民眾關注高溫會產生二噁英。二噁英是常見的持久性環境污染物,能在環境中長存數十年,不受破壞,毒性極高,會導致生育和發育問題、破壞免疫系統、干擾荷爾蒙分泌以及致癌。

香港環保署在中西區和荃灣設監測站,監測空氣中二噁英的濃度,但上月數字至今未更新。香港環保署表示,去年及今年至9月的數據顯示,二噁英日均濃度是每立方米0.009至0.086皮克,並沒發現有異常情況出現。香港環保署表示,現在沒有空氣中二噁英的濃度指引,但過去3年香港取得的二噁英濃度,遠低於加拿大安大略省的二噁英每立方米0.1皮克,和日本每立方米0.6皮克的指標。

參選「啟德中及南」選區候選人梁咏欣表示,近日收到居民反映,指有大量懷疑含有「二噁英」的雜物,被棄置在距離啟德約400米的宏照道路政署地盤內。她要求當局提供環境調查報告及實地分析數據,以確保附近環境不被有害物質污染。

香港環保署稱已將上月份收集的二噁英樣本,送交政府化驗所作化學分析,一般需數星期,預期於本月(11月)底會完成,收到報告後會盡快公布。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?