SpringBoot - 整合SpringSecurity

一、引入相關依賴

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.1.3.RELEASE</version>
</parent>

<properties>
	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
	<java.version>1.8</java.version>
</properties>

<dependencies>
	<!-- Thymeleaf-Security -->
	<dependency>
		<groupId>org.thymeleaf.extras</groupId>
		<artifactId>thymeleaf-extras-springsecurity5</artifactId>
		<version>3.0.4.RELEASE</version>
	</dependency>
	<!-- Thymeleaf -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>
	<!-- SpringSecurity -->
	<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.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.6</version>
	</dependency>
</dependencies>

二、基礎準備

1、準備Pojo類
@Data
public class LcyUser {
    private Long id;
    private String username;
    private String password;
}
2、Service層準備
@Service
public class UserServiceImpl implements UserService {
    @Override
    public LcyUser findUserByUsername(String username) {
        LcyUser user = new LcyUser();
        user.setUsername(username);
        // 對123加密後的密碼
        user.setPassword("$2a$10$XcigeMfToGQ2bqRToFtUi.sG1V.HhrJV6RBjji1yncXReSNNIPl1K");
        return user;
    }
}
3、Controller層準備
@Controller
public class TestController {

    private final String PREFIX = "pages/";

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

    @GetMapping("/loginHtml")
    public String loginPage(){
        return PREFIX + "login";
    }

    @GetMapping("/403")
    public String fourZeroThree(){
        return PREFIX + "403";
    }

    /**
     * level1頁面映射
     * @param path
     * @return
     */
    @GetMapping("/level1/{path}")
    public String level1(@PathVariable("path")String path) {
        return PREFIX+"level1/"+path;
    }

    /**
     * level2頁面映射
     * @param path
     * @return
     */
    @GetMapping("/level2/{path}")
    public String level2(@PathVariable("path")String path) {
        return PREFIX+"level2/"+path;
    }

    /**
     * level3頁面映射
     * @param path
     * @return
     */
    @GetMapping("/level3/{path}")
    public String level3(@PathVariable("path")String path) {
        return PREFIX+"level3/"+path;
    }

    /**
     * level3頁面映射
     * @param path
     * @return
     */
    @GetMapping("/level4/{path}")
//    @Secured("ROLE_ADMIN")    // 擁有ADMIN角色可訪問
//    @PreAuthorize("hasRole('ROLE_ADMIN') and hasRole('ROLE_VIP1')")   // 擁有ADMIN和VIP1可以訪問
//    @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_VIP2')")   // 擁有其中一個就可以訪問
//    @PostAuthorize("hasAnyRole('ROLE_VIP2','ROLE_VIP1')")   // 與@PreAuthorize用法差不多
    public String level4(@PathVariable("path")String path) {
        return PREFIX+"level3/"+path;
    }
}
4、前端頁面準備

前端頁面

  1. 登錄頁面login.html
<body>
	<h1 align="center">歡迎登陸英雄聯盟管理系統</h1>
	<hr>
	<div align="center">
		<form th:action="@{/userLogin}" method="post">
			用戶名:<input name="username"/><br>
			密碼:<input name="password"><br/>
			<input type="checkbox" name="remeber"> 記住我<br/>
			<input type="submit" value="登陸">
		</form>
	</div>
</body>
  1. 主頁面welcome.html
<!DOCTYPE html>
<!-- 注意引入的是SpringSecurity5 -->
<html xmlns:th="http://www.thymeleaf.org"
	  xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1 align="center">歡迎光臨英雄聯盟管理系統</h1>
<!-- 沒認證顯示 -->
<div sec:authorize="!isAuthenticated()">
	<h2 align="center">遊客您好,如果想查看聯盟 <a th:href="@{/userLogin}">請登錄</a></h2>
</div>
<!-- 認證顯示 -->
<div sec:authorize="isAuthenticated()">
	<!-- 賬號與角色 -->
	<h2><span sec:authentication="name"></span>,您好,您的角色有:
		<span sec:authentication="principal.authorities"></span></h2>
	<form th:action="@{/userLogout}" method="post">
		<input type="submit" value="註銷"/>
	</form>
</div>
<hr>
<!-- 擁有VIP1角色可以訪問 -->
<div sec:authorize="hasRole('VIP1')">
	<h3>諾克薩斯陣營</h3>
	<ul>
		<li><a th:href="@{/level1/1}">德萊厄斯</a></li>
		<li><a th:href="@{/level1/2}">卡特琳娜</a></li>
		<li><a th:href="@{/level1/3}">塞恩</a></li>
	</ul>

</div>
<!-- 擁有VIP2角色可以訪問 -->
<div sec:authorize="hasRole('VIP2')">
	<h3>德瑪西亞陣營</h3>
	<ul>
		<li><a th:href="@{/level2/1}">嘉文四世</a></li>
		<li><a th:href="@{/level2/2}">蓋倫</a></li>
		<li><a th:href="@{/level2/3}">拉克絲</a></li>
	</ul>
</div>
<!-- 擁有VIP3角色可以訪問 -->
<div sec:authorize="hasRole('VIP3')">
	<h3>艾歐尼亞陣營</h3>
	<ul>
		<li><a th:href="@{/level3/1}">亞索</a></li>
		<li><a th:href="@{/level3/2}">李青</a></li>
		<li><a th:href="@{/level3/3}">艾瑞莉婭</a></li>
	</ul>
</div>
</body>
</html>

三、自定義認證類

自定義認證類,用於登錄認證使用,需要從數據庫獲取數據進行認證,並授權。需要實現UserDetailsService接口。

public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String usernmae) throws UsernameNotFoundException {
        // 定義權限的集合 - 全部存儲的是權限
        List<GrantedAuthority> authorities = new ArrayList<>();
        // 權限應該從數據庫中獲取,這裏寫死了,權限必須以ROLE_開頭
        authorities.add(new SimpleGrantedAuthority("ROLE_VIP1"));
        // 判斷用戶名是否爲空
        if (StringUtils.isEmpty(usernmae)) {
            return null;
        }
        // 去數據庫中查找對象
        LcyUser lcyUser = userService.findUserByUsername(usernmae);
        // 封裝到User對象中去
        User user = new User(usernmae,lcyUser.getPassword(),authorities);
        return user;
    }
}

四、核心配置類

核心配置類,主要對數據來源和加密攔截登錄/註銷記住我進行了配置。

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)  // 開啓註解配置方法安全
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	/**
	 * 配置哪些請求不攔截
	 */
	@Override
	public void configure(WebSercurity web) throws Exception{
		web.ignoring().antMatchers("/api/**","/swgger-ui.html");
	}
	

    /**
     * 定製請求的授權規則
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 訪問權限設置
        http.authorizeRequests().antMatchers("/").permitAll()    // 任何人都可以訪問
                .antMatchers("/level1/**").hasRole("VIP1")       // VIP1角色可以訪問
                .antMatchers("/level2/**").hasRole("VIP2")       // VIP2角色可以訪問
                .antMatchers("/level3/**").hasRole("VIP3");      // VIP3角色可以訪問
//                .antMatchers("/level4/**")
//                .access("hasRole('ADMIN') and hasRole('VIP3')") // 同時擁有ADMIN和VIP3可以訪問
//                .antMatchers("/level5/**")
//                .access("hasAnyRole('VIP1','VIP3')")				// 擁有VIP1或VIP3可以訪問
//                .anyRequest().authenticated(); // 表示除去上面的,其他URL必須通過認證纔可以訪問

        /**
         * 開啓自動配置的登陸功能,如果沒有登陸/沒有權限就會來到登陸頁面
         * usernameParameter:指定form表單中賬號的name,默認爲username
         * passwordParameter:指定form表單中密碼的name,默認爲password
         * loginPage:指定自定義登錄頁,如果不設置,默認爲Security自帶登錄頁
         * loginProcessingUrl:指定登錄請求url,默認爲/login
         * failureUrl:指定登錄失敗跳轉url,默認爲指定的loginPage?error,即loginHtml?error
         */
        http.formLogin().usernameParameter("username").passwordParameter("password")
                .loginPage("/loginHtml").loginProcessingUrl("/userLogin")
                .failureUrl("/loginHtml?error=true");
        /**
         * 開啓自動配置的註銷功能
         * logoutUrl:註銷請求url,默認爲/logout
         * logoutSuccessUrl:註銷成功跳轉rul,默認爲/loginPage?logout,即這裏的loginHtml?logout
         * clearAuthentication:是否清除身份認證信息,默認爲true
         * invalidateHttpSession:是否使 Session 失效,默認爲true
         */
        http.logout().logoutUrl("/userLogout").logoutSuccessUrl("/")
                     .clearAuthentication(true).invalidateHttpSession(true);

        /**
         * 開啓記住我功能 - 參數爲記住我複選框的name
         * 登陸成功以後,將cookie發給瀏覽器保存,以後訪問頁面帶上這個cookie,只要通過檢查就可以免登錄
         * 點擊註銷會刪除cookie
         */
        http.rememberMe().rememberMeParameter("remeber");
        // 訪問無權限的請求,跳轉的頁面
        http.exceptionHandling().accessDeniedPage("/403");


        // 上面的登錄成功/失敗、註銷、無權限都可以通過處理類來做
        // 成功處理類
//        http.formLogin().successHandler(new AuthenticationSuccessHandlerImpl());
        // 失敗處理類 - 實現AuthenticationFailureHandler
//         http.formLogin().failureForwardUrl(new AuthenticationFailureHandlerImpl());
        // 註銷處理類 - 實現LogoutSuccessHandler
//        http.logout().addLogoutHandler(new LogoutSuccessHandlerImpl());
        // 無權限處理類
//        http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler());
    }

    /**
     * 設置自定義數據源及加密方式
     */
    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception{
        builder.userDetailsService(userDetailService())
                .passwordEncoder(passwordEncoder());
    }

    /**
     * 自定義數據源
     * @return
     */
    @Bean
    public UserDetailServiceImpl userDetailService(){
        return new UserDetailServiceImpl();
    }

    /**
     * 加密方法 - BCrypt
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        /**
         * 可選參數:strength
         * strength 越大,密鑰的迭代次數越多,密鑰的迭代次數爲2的strength次方
         * strength 取值在 4-31 之間,默認爲10(不寫參數的情況下)
         */
        return new BCryptPasswordEncoder();
    }
}

五、自定義登錄成功/失敗處理類

這裏以成功示例:

public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("登錄成功");
        // 手動跳轉
        httpServletResponse.sendRedirect("/admin/index.html");    // 重定向
        /*
            httpServletRequest.getRequestDispatcher("/admin/index.html")
                    .forward(httpServletRequest,httpServletResponse);  // 請求轉發
        */

    }
}

六、角色繼承

前面所定義的VIP1VIP2VIP3,它們之間是沒有任何關係的。但是,有時候我們需要這樣一個需求。VIP1擁有VIP2的權限,VIP2擁有VIP3的權限,這時我們可以通過角色繼承的方式來做。我們只需要在SpringSecurity的配置文件裏添加一個RoleHierarchy的方法即可。

@Bean
public RoleHierarchy roleHierarchy(){
	RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
	// 角色繼承 - VIP1擁有VIP2的角色,VIP2擁有VIP3的角色
	String hierarchy = "ROLE_VIP1 > ROLE_VIP2 ROLE_VIP2 > ROLE_VIP3";
	roleHierarchy.setHierarchy(hierarchy);
	return roleHierarchy;
}

但是,經過我測試,在Thymeleaf頁面使用類似sec:authorize="hasRole('VIP3')"的標籤,VIP1即使擁有VIP3的角色,它也是無法看到標籤所標記的內容的,但是卻是可以訪問VIP3所能訪問的URL

動態配置權限

如果覺得基於HttpSecurity配置的認證授權規則不夠靈活,無法實現資源與角色之間的動態調整。要實現動態配置URL權限,需要自己自定義權限配置。可以在數據配置,賬戶與角色之間的關係,角色與URL的關係來完成。

ROLE_VIP1    /level1/**
ROLE_VIP2    /level2/**

一、準備工作

1、準備POJO類
@Data
public class Role {
    private Integer id;
    /** 角色ROLE */
    private String name;
    /** 角色名,如經理、HR */
    private String rname;

    public Role(Integer id, String name, String rname) {
        this.id = id;
        this.name = name;
        this.rname = rname;
    }
}
@Data
public class Menu {
    private Integer id;
    /** 可訪問url */
    private String pattern;
    /** 可以訪問的角色有哪些 */
    private List<Role> roles;

    public Menu(Integer id, String pattern) {
        this.id = id;
        this.pattern = pattern;
    }
}
2、準備Service類
@Service
public class UserServiceImpl implements UserService {
    @Override
    public List<Menu> getAllMenus() {
        // URL和角色都應該從數據庫獲取,這裏爲了方便,寫死了
        // URL
        Menu menu1 = new Menu(1,"/level1/**");
        Menu menu2 = new Menu(2,"/level2/**");
        List<Menu> list = new ArrayList<>();
        // 角色
        Role role1 = new Role(1,"ROLE_VIP1","VIP1玩家");
        Role role2 = new Role(1,"ROLE_VIP1","VIP1玩家");
        List<Role> roles = new ArrayList<>();
        roles.add(role1);
        roles.add(role2);
        // 角色與URL關係
        menu1.setRoles(roles);
        menu1.setRoles(roles);
        list.add(menu1);
        list.add(menu2);
        return list;
    }

二、自定義FilterInvocationSecurityMetadataSource

配置了動態權限,一個請求會先經過這個類的getAttributes方法進行判斷返回角色。

// 一個請求先走FilterInvocationSecurityMetadataSource,然後再走AccessDecisionManager
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired
    private UserService userService;

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    /**
     * 可以從FilterInvocation中獲取當前請求的URL
     * @return URL所需角色
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        // 獲取資源信息Menu,建議放在Redis等緩存數據庫中
        List<Menu> allMenus = userService.getAllMenus();
        // 獲取當前請求的URL
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        // 所有人都可以訪問的URL,做特殊處理
        if("/".equals(requestUrl) || "/loginHtml".equals(requestUrl) || "/loginHtml?error=true".equals(requestUrl)){
            return SecurityConfig.createList("ROLE_ALL");
        }
        // 遍歷資源信息
        for (Menu menu : allMenus) {
            if (antPathMatcher.match(menu.getPattern(),requestUrl)){
                List<Role> roles = menu.getRoles();
                // 獲取所有角色名 類似於['VIP1','VIP2','VIP3']
                String[] roleArr = new String[roles.size()];
                for (int i = 0; i < roleArr.length; i++){
                    roleArr[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(roleArr);
            }
        }
        // 如果不存在相應的模式,直接返回ROLE_LOGIN,表示登錄就可以訪問
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    /**
     * 返回定義好的權限資源
     * SpringSecurity啓動時會校驗相關配置是否正確
     * 如果不需要校驗,直接返回null
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * 返回類對象是否支持校驗
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

三、自定義AccessDecisionManager

當一個請求走完FilterInvocationSecurityMetadataSource之後就會來到這裏,這裏主要是判斷是否有權限的,有權限纔會正常走請求流程。

// 一個請求先走FilterInvocationSecurityMetadataSource,然後再走AccessDecisionManager
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    /**
     * 判斷當前登錄的用戶是否具備當前請求URL所需要的角色信息
     * 如不具備拋出AccessDeniedException異常
     * 否則不做任何事
     * @param authentication 當前登錄用戶信息
     * @param o FilterInvocation對象,可以獲取當前請求對象
     * @param collection 請求當前URL所需要角色
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (ConfigAttribute configAttribute : collection) {
            // 對所有人可訪問做處理
        	if("ROLE_ALL".equals(configAttribute.getAttribute())){
                return;
            }
            // 登錄即可訪問
            if("ROLE_LOGIN".equals(configAttribute.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken){
                return;
            }
            // 角色對比
            for (GrantedAuthority authority : authorities) {
                if(configAttribute.getAttribute().equals(authority.getAuthority())){
                    return;
                }
            }
        }
        // 可以通過處理該異常來進行跳轉 - 配置http.exceptionHandling()
        throw new AccessDeniedException("權限不足");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

四、配置文件

把上面配置文件靜態的http.authorizeRequests().antMatches這些代碼替換成動態的http.authorizeRequests().withObjectPostProcessor並且將兩個類注入進來即可,其他不用變。

public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 定製請求的授權規則
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(mfisms());
                        o.setAccessDecisionManager(madm());
                        return o;
                    }
                });
    
    @Bean
    public MyFilterInvocationSecurityMetadataSource mfisms(){
        return new MyFilterInvocationSecurityMetadataSource();
    }
    
    @Bean
    public MyAccessDecisionManager madm(){
        return new MyAccessDecisionManager();
    }
注意事項:

動態權限,我目前還沒使用過,經過測試。發現下面代碼類似於antMatchers("/").permitAll()匹配的URL,實際是不生效的,還是要通過withObjectPostProcessor來處理,也就是說大家都可以訪問的請求,需要在FilterInvocationSecurityMetadataSourcegetAttributes方法中作處理。而且,我上面自定義的錯誤跳轉URL,即http.formLogin().failureUrl("/loginHtml?error=true")也會進入withObjectPostProcessor,這就讓我很鬱悶了,百度了很久未解決。

http.authorizeRequests().antMatchers("/").permitAll()
		.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
			@Override
			public <O extends FilterSecurityInterceptor> O postProcess(O o) {
				o.setSecurityMetadataSource(mfisms());
				o.setAccessDecisionManager(madm());
				return o;
			}
		});

也就是說靜態資源同樣也會經過這個withObjectPostProcessor,而且經過測試,發現角色繼承同樣失效。
我思考了一會兒,想來只能把靜態資源大家都能訪問的URL賦給一個角色,每個用戶都有這個共有的角色即可。如果用戶在沒有登錄的情況下訪問,即這時因爲沒有登錄,也就沒有角色,那就只能在FilterInvocationSecurityMetadataSourcegetAttributes方法裏想辦法給它安排這個公共角色或者在AccessDecisionManager裏處理也行。
當然這些都只是我個人的處理方法,因爲技術水平未達到,也沒有閱讀過源碼(水平不夠),只能這樣猜測+實驗式處理了。
如果後面遇到需要處理這種問題且找到解決方案,會更新該博文。

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