1. 程式人生 > 實用技巧 >springboot學習(七)安全管理 spring security

springboot學習(七)安全管理 spring security

Spring Security入門

Spring Security 是 Spring 家族中的一個安全管理框架,Spring Boot 對於 Spring Security 提供了 自動化配置方案,可以零配置使用 Spring Security

  • 新增依賴,只要加入依賴,專案的所有介面都會被自動保護起來,訪問系統會先需要登入認證

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

預設的使用者名稱為user,密碼在控制檯,每次啟動專案都會變,對登入的使用者名稱/密碼進行配置,有三種不同的方式:

  1. 在 application.properties 中進行配置

  2. 通過 Java 程式碼配置在記憶體中

  3. 通過 Java 從資料庫中載入

  • 使用者名稱密碼配置

    • 在 application.properties 中配置

    spring.security.user.name=hjy
    spring.security.user.password=123456

    配置完成後,重啟專案,就可以使用這裡配置的使用者名稱/密碼登入了

    • 在Java程式碼中配置

    建立一個 Spring Security 的配置類,繼承自 WebSecurityConfigurerAdapter 類,使用BCryptPasswordEncoder 進行密碼加密(從 Spring5 開始,強制要求密碼要加密)

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //配置兩個使用者,密碼都加密
    auth.inMemoryAuthentication() .withUser("hjy1").roles("admin").password("$2a$10$D5GuLLF.OOzP28g9Xy1FKu82dj044JeFsNLpujm8sM7xti4IWCTju")
    .and()
    .withUser("hjy2").roles("user").password("$2a$10$4HMMAfpD0xkwq15ceMY4/OZtlETHvLGhCJox3O1Cn9XmsqAUTLxZq");
    }
    @Bean
    PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
    }
    }

    Spring Security 中提供了 BCryptPasswordEncoder 密碼編碼工具,可以非常方便的實現密碼的加密加鹽,相同明文加密出來的結果總是不同,這樣就不需要使用者去額外儲存 鹽的欄位了

    • 登入配置

    對於登入介面,登入成功後的響應,登入失敗後的響應,我們都可以在 WebSecurityConfigurerAdapter 的實現類中進行配置

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Autowired
    VerifyCodeFilter verifyCodeFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class);
    http.authorizeRequests()//開啟登入配置
    .antMatchers("/hello").hasRole("admin")//表示訪問 /hello 這個介面,需要具備 admin 這個角色
    .anyRequest().authenticated()//表示剩餘的其他介面,登入之後就能訪問
    .and()
    .formLogin()
    .loginPage("/login_p")//定義登入頁面,未登入時,訪問一個需要登入之後才能訪問的介面,會自動跳轉到該頁面
    .loginProcessingUrl("/doLogin")//登入處理介面
    .usernameParameter("uname")//定義登入時,使用者名稱的 key,預設為 username
    .passwordParameter("passwd")//定義登入時,使用者密碼的 key,預設為 password
    .successHandler(new AuthenticationSuccessHandler(){//登入成功的處理器
    @Override
    public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication)throws IOException,ServletException{
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("success");
    out.flush();
    }
    })
    .failureHandler(new AuthenticationFailureHandler(){
    @Override
    public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException exception) throws IOException,ServletException{
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("fail");
    out.flush();
    }
    })
    .permitAll()//和表單登入相關的介面統統都直接通過
    .and()
    .logout()
    .logoutUrl("/logout")
    .logoutSuccessHandler(new LogoutSuccessHandler(){
    @Override
    public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication)throws IOException,ServletException{
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("logout success");
    out.flush();
    }
    })
    .permitAll()
    .and()
    .httpBasic()
    .and()
    .csrf()
    .disable();
    }
    }

    VerifyCodeFilter是自定義的圖片驗證碼,參考圖片驗證碼

    可以在 successHandler 方法中,配置登入成功的回撥,如果是前後端分離開發的話,登入成功後返回 JSON 即可,同理,failureHandler 方法中配置登入失敗的回撥,logoutSuccessHandler 中則配置登出成功的回撥

    • 忽略攔截

    如果某一個請求地址不需要攔截的話,過濾掉該地址,即該地址不走 Spring Security 過濾器鏈

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
    public void configure(WebSecurity web) throws Exception{
    web.ignoring().antMatchers("/vercode");
    }
    }

Spring Security新增驗證碼

  • 驗證碼工具類

public class VerifyCode {
private int width = 100;// 生成驗證碼圖片的寬度
private int height = 50;// 生成驗證碼圖片的高度
private String[] fontNames = {"宋體", "楷體", "隸書", "微軟雅黑"};
private Color bgColor = new Color(255, 255, 255);// 定義驗證碼圖片的背景顏色為白色
private Random random = new Random();
private String codes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private String text;// 記錄隨機字串
/**
* 獲取一個隨意顏色
* @return
*/
private Color randomColor() {
int red = random.nextInt(150);
int green = random.nextInt(150);
int blue = random.nextInt(150);
return new Color(red, green, blue);
}
/**
* 獲取一個隨機字型
* @return
*/
private Font randomFont() {
String name = fontNames[random.nextInt(fontNames.length)];
int style = random.nextInt(4);
int size = random.nextInt(5) + 24;
return new Font(name, style, size);
}
/**
* 獲取一個隨機字元
* @return
*/
private char randomChar() {
return codes.charAt(random.nextInt(codes.length()));
}
/**
* 建立一個空白的BufferedImage物件
* @return
*/
private BufferedImage createImage() {
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setColor(bgColor);// 設定驗證碼圖片的背景顏色
g2.fillRect(0, 0, width, height);
return image;
}
public BufferedImage getImage() {
BufferedImage image = createImage();
Graphics2D g2 = (Graphics2D) image.getGraphics();
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 4; i++) {
String s = randomChar() + "";
sb.append(s);
g2.setColor(randomColor());
g2.setFont(randomFont());
float x = i * width * 1.0f / 4;
g2.drawString(s, x, height - 15);
}
this.text = sb.toString();
drawLine(image);
return image;
}
/**
* 繪製干擾線
* @param image
*/
private void drawLine(BufferedImage image) {
Graphics2D g2 = (Graphics2D) image.getGraphics();
int num = 5;
for(int i = 0; i < num; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g2.setColor(randomColor());
g2.setStroke(new BasicStroke(1.5f));
g2.drawLine(x1, y1, x2, y2);
}
}
public String getText() {
return text;
}
public static void output(BufferedImage image, OutputStream out) throws IOException {
ImageIO.write(image, "JPEG", out);
}
}
  • 驗證碼controller

@RestController
public class VerifyController {
@GetMapping("/verifyCode")
public void code(HttpServletRequest request, HttpServletResponse response) throws IOException {
VerifyCode code=new VerifyCode();
BufferedImage image=code.getImage();
String text=code.getText();
HttpSession session=request.getSession();
session.setAttribute("index_code",text);
VerifyCode.output(image,response.getOutputStream());
}
}

建立了一個VerifyCode物件,將生成的驗證碼字元儲存到session中,然後通過流將圖片寫到前端,img標籤如下

<img src="/verifyCode" alt="">
  • 自定義過濾器

@Component
public class VerifyCodeFilter extends GenericFilterBean {
private String defaultFilterProcessUrl="/doLogin";

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request=(HttpServletRequest)servletRequest;
HttpServletResponse response=(HttpServletResponse)servletResponse;
if("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())){
//驗證碼驗證
String requestCaptcha=request.getParameter("code");
String genCaptcha=(String)request.getSession().getAttribute("index_code");
if(StringUtils.isEmpty(requestCaptcha)){
throw new AuthenticationException("驗證碼不能為空");
}
if(!genCaptcha.equalsIgnoreCase(requestCaptcha)){
throw new AuthenticationException("驗證碼錯誤");
}
filterChain.doFilter(request,response);
}
}
}

自定義過濾器繼承自GenericFilterBean,並實現其中的doFilter方法,在doFilter方法中,當請求方法是POST,並且請求地址是 /doLogin時,獲取引數中的code欄位值,該欄位儲存了使用者從前端頁面傳來的驗證碼,然後獲取session中儲存的驗證碼,如果使用者沒有傳來驗證碼,則丟擲驗證碼不能為空異常,如果使用者傳入了驗證碼,則判斷驗證碼是否正確,如果不正確則丟擲異常,否則執行 chain.doFilter(request,response);使請求繼續向下走

  • 在WebSecurityConfigurerAdapter 的實現類中進行配置,見入門登入配置

Spring Security登入使用json

通過分析原始碼我們發現,預設的使用者名稱密碼提取在UsernamePasswordAuthenticationFilter過濾器中,如果想將使用者名稱密碼通過JSON的方式進行傳遞,則需要自定義相關過濾器將其替換即可

自定義過濾器:將使用者名稱/密碼的獲取方案重新修正下,改為了從JSON中獲取使用者名稱密碼

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try(InputStream is = request.getInputStream()) {
Map < String, String > authenticationBean = mapper.readValue(is, Map.class);
authRequest = new UsernamePasswordAuthenticationToken(authenticationBean.get("username"), authenticationBean.get("password"));
} catch(IOException e)
{ e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken("", "");
} finally
{ setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
} else {
return super.attemptAuthentication(request, response);
}
}
}

在SecurityConfig(WebSecurityConfigurerAdapter 實現類)中,將自定義的CustomAuthenticationFilter類加入進來即可

  @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable();
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.ok("登入成功!");
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("登入失敗!");
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}

SpringSecurity中的角色繼承

角色繼承實際上是一個很常見的需求,因為大部分公司治理可能都是金字塔形的,上司可能具備下屬的部分甚至所有許可權,這一現實場景,反映到我們的程式碼中,就是角色繼承了,角色繼承關係的解析在RoleHierarchyImpl類的buildRolesReachableInOneStepMap方法中

配置角色的繼承關係:

  @Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}

提供了一個RoleHierarchy介面的例項,使用字串來描述了角色之間的繼承關係, ROLE_dba具備 ROLE_admin的所有許可權,而 ROLE_admin則具備 ROLE_user的所有許可權,繼承與繼承之間用一個換行符隔開。提供了這個Bean之後,以後所有具備 ROLE_user角色才能訪問的資源, ROLE_dbaROLE_admin也都能訪問,具備 ROLE_amdin角色才能訪問的資源, ROLE_dba也能訪問

在SecurityConfig(WebSecurityConfigurerAdapter 實現類)中指定角色和資源的對應關係即可

  @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("admin")
.antMatchers("/db/**")
.hasRole("dba")
.antMatchers("/user/**")
.hasRole("user")
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
.csrf()
.disable();
}

這個表示 /db/**格式的路徑需要具備dba角色才能訪問, /admin/**格式的路徑則需要具備admin角色才能訪問, /user/**格式的路徑,則需要具備user角色才能訪問,此時提供相關介面,會發現,dba除了訪問 /db/**,也能訪問 /admin/**/user/**,admin角色除了訪問 /admin/**,也能訪問 /user/**,user角色則只能訪問 /user/**

SpringSecurity中使用JWT

  • 無狀態是什麼

    • 微服務叢集中的每個服務,對外提供的都使用RESTful風格的介面。而RESTful風格的一個最重要的規範就是:服務的無狀態性,即:

      • 服務端不儲存任何客戶端請求者資訊

      • 客戶端的每次請求必須具備自描述資訊,通過這些資訊識別客戶端身份

      那麼這種無狀態性有哪些好處呢?

      • 客戶端請求不依賴服務端的資訊,多次請求不需要必須訪問到同一臺伺服器

      • 服務端的叢集和狀態對客戶端透明

      • 服務端可以任意的遷移和伸縮(可以方便的進行叢集化部署)

      • 減小服務端儲存壓力

  • 無狀態登入流程

    • 首先客戶端傳送賬戶名/密碼到服務端進行認證

    • 認證通過後,服務端將使用者資訊加密並且編碼成一個token,返回給客戶端

    • 以後客戶端每次傳送請求,都需要攜帶認證的token

    • 服務端對客戶端傳送來的token進行解密,判斷是否有效,並且獲取使用者登入資訊

JWT,全稱是Json Web Token, 是一種JSON風格的輕量級的授權和身份認證規範,可實現無狀態、分散式的Web應用授權

JWT包含三部分資料:

1.Header:頭部,通常頭部有兩部分資訊:

  • 宣告型別,這裡是JWT

  • 加密演算法,自定義

我們會對頭部進行Base64Url編碼(可解碼),得到第一部分資料。

2.Payload:載荷,就是有效資料,在官方文件中(RFC7519),這裡給了7個示例資訊:

  • iss (issuer):表示簽發人

  • exp (expiration time):表示token過期時間

  • sub (subject):主題

  • aud (audience):受眾

  • nbf (Not Before):生效時間

  • iat (Issued At):簽發時間

  • jti (JWT ID):編號

這部分也會採用Base64Url編碼,得到第二部分資料。

3.Signature:簽名,是整個資料的認證資訊。一般根據前兩步的資料,再加上服務的的金鑰secret(金鑰儲存在服務端,不能洩露給客戶端),通過Header中配置的加密演算法生成。用於驗證整個資料完整和可靠性。

生成的資料格式如下圖:

注意,這裡的資料通過 . 隔開成了三部分,分別對應前面提到的三部分,另外,這裡資料是不換行的,圖片換行只是為了展示方便而已。

因為JWT簽發的token中已經包含了使用者的身份資訊,並且每次請求都會攜帶,這樣服務的就無需儲存使用者資訊,甚至無需去資料庫查詢,這樣就完全符合了RESTful的無狀態規範。

JWT的問題

  1. 續簽問題,這是被很多人詬病的問題之一,傳統的cookie+session的方案天然的支援續簽,但是jwt由於服務端不儲存使用者狀態,因此很難完美解決續簽問題,如果引入redis,雖然可以解決問題,但是jwt也變得不倫不類了。

  2. 登出問題,由於服務端不再儲存使用者資訊,所以一般可以通過修改secret來實現登出,服務端secret修改後,已經頒發的未過期的token就會認證失敗,進而實現登出,不過畢竟沒有傳統的登出方便。

  3. 密碼重置,密碼重置後,原本的token依然可以訪問系統,這時候也需要強制修改secret。

  4. 基於第2點和第3點,一般建議不同使用者取不同secret。

  • 新增依賴

    <dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
  • 建立User類實現UserDetails 介面

public class User implements UserDetails {
private String username;
private String password;
private List < GrantedAuthority > authorities;
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
//省略getter/setter
}
  • JMT過濾器配置

提供兩個和 JWT 相關的過濾器配置:

  1. 一個是使用者登入的過濾器,在使用者的登入的過濾器中校驗使用者是否登入成功,如果登入成功,則生成一個token返回給客戶端,登入失敗則給前端一個登入失敗的提示。

  2. 第二個過濾器則是當其他請求傳送來,校驗token的過濾器,如果校驗成功,就讓請求繼續執行

    • 登入過濾器

    public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
    protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
    super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
    setAuthenticationManager(authenticationManager);
    }
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException {
    User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
    return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    Collection <? extends GrantedAuthority > authorities = authResult.getAuthorities();
    StringBuffer as = new StringBuffer();
    for(GrantedAuthority authority: authorities) {
    as.append(authority.getAuthority()).append(",");
    }
    String jwt = Jwts.builder()
    .claim("authorities", as)//配置使用者角色
    .setSubject(authResult.getName())
    .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
    .signWith(SignatureAlgorithm.HS512, "sang@123")
    .compact();
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write(new ObjectMapper().writeValueAsString(jwt));
    out.flush();
    out.close();
    }
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
    resp.setContentType("application/json;charset=utf-8");
    PrintWriter out = resp.getWriter();
    out.write("登入失敗!");
    out.flush();
    out.close();
    }
    }

    解析:

    1. 自定義 JwtLoginFilter 繼承自 AbstractAuthenticationProcessingFilter,並實現其中的三個預設方法。

    2. attemptAuthentication方法中,我們從登入引數中提取出使用者名稱密碼,然後呼叫AuthenticationManager.authenticate()方法去進行自動校驗。

    3. 第二步如果校驗成功,就會來到successfulAuthentication回撥中,在successfulAuthentication方法中,將使用者角色遍歷然後用一個 , 連線起來,然後再利用Jwts去生成token,按照程式碼的順序,生成過程一共配置了四個引數,分別是使用者角色、主題、過期時間以及加密演算法和金鑰,然後將生成的token寫出到客戶端。

    4. 第二步如果校驗失敗就會來到unsuccessfulAuthentication方法中,在這個方法中返回一個錯誤提示給客戶端即可。

    • token校驗過濾器

    public class JwtFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) servletRequest;
    String jwtToken = req.getHeader("authorization");
    System.out.println(jwtToken);
    Claims claims = Jwts.parser().setSigningKey("sang@123").parseClaimsJws(jwtToken.replace("Bearer", "")).getBody();
    String username = claims.getSubject();
    //獲取當前登入使用者名稱
    List < GrantedAuthority > authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
    SecurityContextHolder.getContext().setAuthentication(token);
    filterChain.doFilter(req, servletResponse);
    }
    }

    解析:

    1. 首先從請求頭中提取出 authorization 欄位,這個欄位對應的value就是使用者的token。

    2. 將提取出來的token字串轉換為一個Claims物件,再從Claims物件中提取出當前使用者名稱和使用者角色,建立一個UsernamePasswordAuthenticationToken放到當前的Context中,然後執行過濾鏈使請求繼續執行下去。

    • Spring Security 配置

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
    .withUser("admin")
    .password("123")
    .roles("admin")
    .and()
    .withUser("sang")
    .password("456")
    .roles("user");
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
    .antMatchers("/hello")
    .hasRole("user")
    .antMatchers("/admin")
    .hasRole("admin")
    .antMatchers(HttpMethod.POST, "/login")
    .permitAll().anyRequest()
    .authenticated()
    .and()
    .addFilterBefore(new JwtLoginFilter("/login", authenticationManager()), UsernamePasswordAuthenticationFilter.class)
    .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
    .csrf()
    .disable();
    }
    }

    解析:

    1. 簡單起見,這裡我並未對密碼進行加密,因此配置了NoOpPasswordEncoder的例項。

    2. 簡單起見,這裡並未連線資料庫,我直接在記憶體中配置了兩個使用者,兩個使用者具備不同的角色。

    3. 配置路徑規則時, /hello 介面必須要具備 user 角色才能訪問, /admin 介面必須要具備 admin 角色才能訪問,POST 請求並且是 /login 介面則可以直接通過,其他介面必須認證後才能訪問。

    4. 最後配置上兩個自定義的過濾器並且關閉掉csrf保護。