1. 程式人生 > >用SpringSecurity從零搭建pc專案-01

用SpringSecurity從零搭建pc專案-01

在java的許可權框架裡,shiro和SpringSecurity是用的最多的。隨著springboot的流行,SpringSecurity也越來越火了,因為springboot預設支援SpringSecurity。所以很有必要把SpringSecurity也搞明白。shiro更加輕量級,SpringSecurity的功能更加豐富。

軟體環境:

  開發工具:Idea

  JDK:1.8   

  maven:3.3.9

     * SpringBoot版本:1.5.18  (說明:springboot版本沒有用最新版的2.0x,因為2.0x和1.5 差別比較大吧,2.x好似是基於spring5的,SpringSecurity的更新速度好似跟不上springboot,所以這裡如果用springboot 2.x會有問題,保險起見這裡就不求新了)

SpringSecurity核心功能:認證、授權、攻擊防護(防止偽造身份)

話不多說,下邊開始打程式碼。

一、專案搭建

1.1  熱身 ,SpringSecurity預設的Httpbasic鑑權

1.1.1,新建專案

填好專案資訊,next

選啟動器:SpringSecurity、Web

設定專案目錄。finish

建好後的專案結構:security包放有關SpringSecurity的程式碼

maven依賴:

<dependencies>
        <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.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>

引入了SpringSecurity的starter後,會自動引入SpringSecurity相關的相關依賴:

 1.1.2 訪問我們的服務

 新建一個Controller:

@RestController
public class UserController {

    @RequestMapping("/hello")
    public String hello(){
        return "Hello ! ";
    }
}

啟動專案,訪問localhost:8080/hello,預設就會出來這個登入框,只要引入了SpringSecurity,springboot就預設會啟用http basic認證,所有的服務都會被保護起來, ,。

其中的預設使用者名稱是user,預設的密碼在啟動專案的時候會在控制檯列印。

輸入使用者名稱密碼,就能訪問:

 

如果輸入的使用者名稱或密碼錯誤,Springboot會引導你到一個空白頁,這個也是可以配置的,後邊再說怎麼配置:

你還可以關閉這個預設的鑑權,只需在application.properties 裡配置:  security.basic.enabled = false ,這個值預設是true。

二、表單登入

如果想去掉那個比較醜的basic登入框,只需要一個配置類即可。新建配置類 BrowserSecurityConfig,繼承  WebSecurityConfigurerAdapter

@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    
}

(為什麼這麼寫?因為SpringSecurity官方文件這麼說的https://docs.spring.io/spring-security/site/docs/4.2.10.RELEASE/reference/htmlsingle/#samples):

    5.1 Hello Web Security Java配置

    第一步是建立Spring Security Java配置。配置建立一個Servlet過濾器,稱為springSecurityFilterChain負責應用程式內所有安全性(保護應用程式URL,驗證提交的使用者名稱和密碼,重定向到登入表單等)。您可以在下面找到Spring Security Java配置的最基本示例:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user").password("password").roles("USER").build());
        return manager;
    }
}
然後啟動application,訪問http://localhost:8080/hello,可以看到已經出來了一個登入頁面,輸入使用者名稱user,以及控制檯列印的密碼:

登入後 訪問的是登入之前的要訪問的hello服務:

官網說明:

到目前為止,我們的WebSecurityConfig僅包含有關如何驗證使用者身份的資訊。Spring Security如何知道我們要求所有使用者進行身份驗證?Spring Security如何知道我們想要支援基於表單的身份驗證?原因是WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中提供了一個預設配置,如下所示:

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .and()
        .httpBasic();
}

三、自定義使用者認證邏輯

3.1 ,處理使用者資訊獲取 、使用者校驗  、密碼的加密解密

新建一個類,MyUserDetailService (這個類必須是Spring裡的一個Bean,所以加上@Component註解)實現 UserDetailsService 介面,UserDetailsService 介面只有一個方法:通過使用者名稱查詢使用者資訊,返回UserDetail

UserDetailsService.java 
public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
UserDetails.java : 提供了幾個方法,賬戶是否啟用、賬戶是否過期、密碼是否過期、賬戶是否鎖定、許可權集合資訊
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

 配置一個密碼的加密解密器類:PasswordEncoder,我們用其一個實現類 BCryptPasswordEncoder,只有兩個方法,一個是加密密碼,一個是匹配方法。如果已有系統已經有了自己的加密演算法,這裡可以換成自己的加解密邏輯。

  @Bean
    public PasswordEncoder passwordencoder(){
        //BCryptPasswordEncoder implements PasswordEncoder
        return new BCryptPasswordEncoder();
    }

這個介面的實現類會給加密的密碼隨機加鹽,所以一樣的密碼每次加密出來是不一樣的,更安全。如123456加密2次:

加密後密碼:  $2a$10$BChH.C4.X8MYuI1mHFoOkefWhOsad7SvhZedHFt1OG4vjSu.z9weC

加密後密碼:  $2a$10$YUbz.miE5C0aAcuU1FnHSu/U.Qm/BujTNw6X7S5i4/6AhjyDc6suK

 此時我們的邏輯是,只要密碼輸入123456,就能登入成功。啟動專案試驗:訪問/hello ,會自動跳轉/login ,隨便輸入使用者名稱aaaaaa,輸入密碼123456 ,成功訪問Hello!

    |     

 3.2 自定義登入頁面

用SpringSecurity提供的預設的登入頁肯定是不行的,所以需要自定義登入頁(當然這裡還可以配置成一個Controller方法,然後跳轉到登入頁。或者從配置檔案裡讀取),在BrowserSecurityConfig類裡配置:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //http.httpBasic() //這個就是預設的彈框認證
        http.formLogin() //表單認證
                .loginPage("/login.html")
                .loginProcessingUrl("/authentication/form")
                .and()
                .authorizeRequests() //下邊的都是授權的配置
                .antMatchers("/login.html").permitAll() //放過登入頁不過濾
                .anyRequest()        //任何請求
                .authenticated();    //都需要身份認證
    }
.loginPage("/login.html") 配置登入頁面
.loginProcessingUrl("/authentication/form") 配置處理登入表單的action,這個值預設在UsernamePasswordAuthenticationFilter 類裡,

   注意一定要放過登入頁不過濾,否則一直不能跳轉到login.html 

此時訪問 /hello ,跳轉到自定義的登入頁:

隨便輸入使用者名稱,密碼輸入123456 ,登入,出現

SpringSecurity預設提供了CSRF(跨站請求偽造)防護,是用CSRF token來完成防護的。暫時關閉CSRF防護,在BrowserSecurityConfig裡設定:

.csrf().disable();
再次登入,可以成功訪問到 /hello服務。
 至此自定義登入頁面完成嗎,下一步自定義登入成功處理。
也可以把登入頁做成在配置檔案application.properties裡配置,然後在程式碼裡讀取配置,這樣更靈活、更通用。
這裡有springboot讀取配置檔案的用法:https://www.cnblogs.com/lihaoyang/p/10223339.html

 3.2 自定義登入成功/失敗處理

現在的流程是,訪問一個需要登入的服務,如果沒有登入,跳轉登入頁,等你登入後,立馬就訪問登入之前的服務。如果你想在登入成功後做一些處理,比如簽到,送積分等等。那就需要自定義登入成功的處理。如果沒有這種需求,這一步驟可以略過。

springsecurity提供了一個介面,AuthenticationSuccessHandler,用來處理登入成功後的邏輯:

 

/**
 * Strategy used to handle a successful user authentication.
 * <p>
 * Implementations can do whatever they want but typical behaviour would be to control the
 * navigation to the subsequent destination (using a redirect or a forward). For example,
 * after a user has logged in by submitting a login form, the application needs to decide
 * where they should be redirected to afterwards (see
 * {@link AbstractAuthenticationProcessingFilter} and subclasses). Other logic may also be
 * included if required.
 *
 * @author Luke Taylor
 * @since 3.0
 */
public interface AuthenticationSuccessHandler {

    /**
     * Called when a user has been successfully authenticated.
     *
     * @param request the request which caused the successful authentication
     * @param response the response
     * @param authentication the <tt>Authentication</tt> object which was created during
     * the authentication process.
     */
    void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException;

}

 

1,定義一個自己的成功處理器,實現這個Handler:

package com.lhy.browser.security.authentication;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 認證成功處理器
 */
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    //springmvc啟動會自動註冊一個ObjectMapper
    @Autowired
    private ObjectMapper objectMapper;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        logger.info("登入成功");
        //把authentication返回給響應
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(authentication));

    }
}

2、新增到配置類,讓spring security執行自定義的處理器

 在 BrowserSecurityConfig  配置類裡注入 AuthenticationSuccessHandler

@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {


    //自定義的認證成功的處理器
    @Autowired
    private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
    //自定義的認證失敗的處理器
    @Autowired
    private AuthenticationFailureHandler myAuthenticationFailureHandler;


    @Autowired
    private SecurityProperties securityProperties;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //http.httpBasic() //這個就是預設的彈框認證
        http.formLogin() //表單認證
                .loginPage(securityProperties.getBrowser().getLoginPage())//登入頁
                .loginProcessingUrl("/authentication/form") //登入提交action
                .successHandler(myAuthenticationSuccessHandler) //自定義的認證成功處理器
                .failureHandler(myAuthenticationFailureHandler)//自定義的認證失敗的處理器
                .and()
                .authorizeRequests() //下邊的都是授權的配置
                .antMatchers(securityProperties.getBrowser().getLoginPage()).permitAll() //放過登入頁不過濾
                .anyRequest()        //任何請求
                .authenticated()    //都需要身份認證
                .and()
                .csrf().disable();
    }


}

登入失敗處理器和登入成功類似,自定義失敗處理類,實現  AuthenticationFailureHandler  介面即可:

/**
 * 認證失敗處理器
 */
@Component("/myAuthenticationFailureHandler")
public class MyAuthenticationFailureHandler  implements AuthenticationFailureHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    //springmvc啟動會自動註冊一個ObjectMapper
    @Autowired
    private ObjectMapper objectMapper;


    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info("登入失敗");

        //把authentication返回給響應
        //狀態碼500,伺服器內部錯誤
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
    }
}

這樣就可以在登入成功和失敗後進入自己的處理邏輯了。