SpringBoot集成Spring Security安全認證框架

從事WEB開發以來,一般用的安全認證框架,最多就是有兩種:Apache Shrio和Spring Security,而Spring Security作爲Spring全家桶中的一員,相對於Spring集成起來更具有優勢和更好的支持!

 

Spring Security是一個強大且可定製的身份驗證和訪問控制框架,完全基於Spring的應用程序標準,它能夠爲基於Spring的企業應用系統提供安全訪問控制解決方案的安全框架,它提供了一組可以在Spring應用上下文中配置的bean,充分利用了Spring IoC和AOP功能。用過apache shrio安全框架的碼友們都知道,安全認證框架主要包含兩個操作:認證(Authentication)和授權(Authorization)。Spring Security基於上述的兩個操作,也是提供了多個模塊:

1、核心模塊(spring-security-core.jar):包含核心的驗證和訪問控制類和接口,遠程支持和基本的配置API。任何使用Spring Security的應用程序都需要這個模塊。支持獨立應用程序、遠程客戶端、服務層方法安全和JDBC用戶配置。

2、遠程調用(spring-security-remoting.jar):提供與Spring Remoting的集成,通常我們不需要這個模塊,除非你要使用Spring Remoting編寫遠程客戶端。

3、Web網頁(spring-security-web.jar):包含網站安全相關的基礎代碼,包括Spring security網頁驗證服務和基於URL的訪問控制。

4、配置(spring-security-config.jar):包含安全命令空間的解析代碼。如果你使用Spring Security XML命令空間進行配置你需要包含這個模塊。

5、LDAP(spring-security-ldap.jar):LDAP驗證和配置代碼,這個模塊可用於LDAP驗證和管理LDAP用戶實體。

6、ACL訪問控制(spring-security-acl.jar):ACL專門的領域對象的實現。用來在你的應用程序中對特定的領域對象實例應用安全性。

7、CAS(spring-security-cas.jar):Spring Security的CAS客戶端集成。如果你想用CAS的SSO服務器使用Spring Security網頁驗證需要該模塊。

8、OpenID(spring-security-openid.jar):OpenID 網頁驗證支持。使用外部的OpenID服務器驗證用戶。

9、Test(spring-security-test.jar):支持Spring security的測試。

在SpringBoot中配置Spring security中非常簡單,在pom.xml文件中加入Spring security的依賴,由於要使用靜態模板的支持,所以把thymeleaf的依賴也給引入進來:

需要創建一個自定義類繼承WebSecurityConfigurerAdapter:

WebSecurityConfigurerAdapter是Spring security爲Web應用提供的一個適配器,實現了WebSecurityConfigurer接口,提供了兩個方法用於重寫,從而實現開發者需求的安全配置。

方法configure(HttpSecurity http)可以通過http.authorizeRequests()定義哪些URL需要保護、哪些URL不需要,通過formLogin()方法定義當前用戶登陸的時候,跳轉到的登陸頁面。

方法configure(AuthenticationManagerBuilder auth)用於創建用戶和用戶的角色。

該自定義類代碼如下:

package com.datang.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class MySecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Autowired
    MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("MySecurityConfigurer HttpSecurity 調用...");
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/", "/home").hasRole("ROCKET")
                .antMatchers("/admin/**").hasAnyRole("LAKER", "HEAT")
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login")
                .successHandler(new MyAuthenticationSuccessHandler())
                .usernameParameter("username").passwordParameter("password")
                .and()
                .logout().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/accessDenied");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println("MySecurityConfigurer AuthenticationManagerBuilder 調用...");
        auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder())
                .withUser("t-mac").password("rocket1").roles("ROCKET");
        auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder())
                .withUser("james").password("laker23").roles("LAKER", "HEAT");
    }
}

上面代碼中,當登陸成功後會有一個處理,就是登陸成功後怎麼跳轉,也就是我們自己定義的handler:new MyAuthenticationSuccessHandler(),該類的代碼如下:

package com.datang.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Component
public class MyAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    //負責所有重定向事務
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String targetUrl = directTargetUrl(authentication);
        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

    protected String directTargetUrl(Authentication authentication) {
        String url = "";
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        List<String> roles = new ArrayList<>();
        for (GrantedAuthority a: authorities) {
            roles.add(a.getAuthority());
        }
        if (isAdmin(roles)) {
            url = "/admin";
        } else if (isUser(roles)) {
            url = "/home";
        } else {
            url = "/accessDenied";
        }
        System.out.println("url = " + url);
        return url;
    }

    private boolean isUser(List<String> roles) {
        if (roles.contains("ROLE_ROCKET")) {
            return true;
        } else {
            return false;
        }
    }

    private boolean isAdmin(List<String> roles) {
        if (roles.contains("ROLE_LAKER")) {
            return true;
        } else {
            return false;
        }
    }

}

該類繼承了SimpleUrlAuthenticationSuccessHandler,提供了handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)方法用於處理登陸成功後的URL重定向,其中directTargetUrl()方法是要獲取當前登陸者的角色,然後根據角色重定向到指定的URL。

在我們定義的MySecurityConfigurer類中,configure(AuthenticationManagerBuilder auth)方法也出現了一個我們自定義的類:new MyPasswordEncoder(),該類的代碼是:

package com.datang.security;

import org.springframework.security.crypto.password.PasswordEncoder;

public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

它實現了PasswordEncoder接口,並實現了接口的兩個方法:encode()和matches(),這是一個密碼編輯器。

好了,現在基礎工作以及準備的差不多了,現在就寫個controller類來測試一下,首先寫一個基礎的BaseController,該類中實現了獲取用戶名getUsername()和獲取角色getAuthority()的方法,代碼如下:

package com.datang.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.ArrayList;
import java.util.List;

public class BaseController {

    protected String getUsername() {
        String name = SecurityContextHolder.getContext().getAuthentication().getName();
        return name;
    }

    protected String getAuthority() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        List<String> roles = new ArrayList<>();
        for (GrantedAuthority a:authentication.getAuthorities()) {
            roles.add(a.getAuthority());
        }
        return roles.toString();
    }
}

然後寫一個測試類MySecurityController,繼承BaseController,其中代碼如下:

package com.datang.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Controller
public class MySecurityController extends BaseController{

    @RequestMapping("/")
    @ResponseBody
    public String index() {
        return "首頁";
    }

    @RequestMapping("/home")
    @ResponseBody
    public String home() {
        String username = getUsername();
        System.out.println("username = " + username);
        return "主頁 - 用戶名:" + username;
    }

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

    @RequestMapping("/admin")
    @ResponseBody
    public String admin() {
        String username = getUsername();
        String role = getAuthority();
        return "管理頁 - 用戶名:" + username + " - 角色:" + role;
    }

    @RequestMapping("/nba")
    @ResponseBody
    public String nba() {
        String username = getUsername();
        String role = getAuthority();
        return "NBA - 用戶名:" + username + " - 角色:" + role;
    }

    @RequestMapping("/accessDenied")
    @ResponseBody
    public String accessDenied() {
        return "無權限";
    }

    @RequestMapping("/logout")
    @ResponseBody
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }
        return "用戶登出";
    }
}

由於是簡單的測試,所以就湊合寫一個簡單的登陸頁面login.html,其他頁面就用返回字符串來代替!

login.html代碼如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <title>登錄</title>
</head>
<body class="text-center">
<form class="form-signin" th:action="@{/login}" method="post">
    <h1 class="h3 mb-3 font-weight-normal">請登錄</h1>
    <label for="username" class="sr-only">用戶名</label>
    <input type="text" id="username" name="username" class="form-control" placeholder="username" required="" autofocus="">
    <label for="password" class="sr-only">密碼</label>
    <input type="password" id="password" name="password" class="form-control" placeholder="password" required="">
    <button type="submit">登錄</button>
</form>
</body>
</html>

然後整體代碼結構如下,紅色邊框外的類和包自行忽略呀:

啓動這個demo:

看了咱們定義的URL訪問規則和用戶創建生效了,下面可以訪問了,輸入URL:http://localhost:9092/nba或者其他的諸如http://localhost:9092/aaa,會被重定向到http://localhost:9092/login,因爲沒有登陸:

輸入用戶james登陸,會被重定向到/admin,輸入t-mac會被重定向到/home

此時再訪問http://localhost:9092/nba就可以了:

其他登出/logout也是正常登出的,自個兒動手試一下吧!

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章