天天看點

Spring Security入門學習Spring Security入門學習**

Spring Security入門學習**

1.Spring Security概述

Spring Security 是 Spring 家族中的一個安全管理架構,實際上,在 Spring Boot 出現之前,Spring Security 就已經發展了多年了,但是使用的并不多,安全管理這個領域,一直是 Shiro 的天下。

相對于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比較麻煩的操作,是以,Spring Security 雖然功能比 Shiro 強大,但是使用反而沒有 Shiro 多(Shiro 雖然功能沒有 Spring Security 多,但是對于大部分項目而言,Shiro 也夠用了)。

自從有了 Spring Boot 之後,Spring Boot 對于 Spring Security 提供了 自動化配置方案,可以幾乎零配置使用 Spring Security。

2.Spring Security入門案例

1)建立Maven項目

Spring Security入門學習Spring Security入門學習**

2)application.properties配置檔案

server.port=8001
           

3)引入POM依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springsecuritylearn</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

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

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
           

4)主啟動類

package com.hexin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringSecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }
}

           

5)業務接口

package com.hexin.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/security")
public class HelloController {

    @GetMapping("hello")
    public String Hello(){
        return "hello security!";
    }
}

           

6)測試

啟動服務:

通路:http://localhost:8001/security/hello

可以看到如下界面,這是SpringSecurity内置的登入頁面,說明我們內建SpringSecurity成功了。

Spring Security入門學習Spring Security入門學習**

下面我們輸入使用者名和密碼。當我們沒有主動配置使用者名和密碼的時候,預設的使用者名為user,密碼可以在應用啟動日志中檢視到。

Spring Security入門學習Spring Security入門學習**

當我們輸入使用者名和密碼後,就可以正常登陸了:

Spring Security入門學習Spring Security入門學習**

3.Spring Security Web認證授權方案

Spring Security本質上就是一個過濾器鍊。

3.1過濾器鍊

SpringSecurity 本質是一個過濾器鍊:

從項目啟動是可以擷取到過濾器鍊:

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter 
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 
org.springframework.security.web.session.SessionManagementFilter 
org.springframework.security.web.access.ExceptionTranslationFilter 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
           

代碼底層流程:重點看三個過濾器:

3.1.1FilterSecurityInterceptor

​ 這是一個方法級别的權限過濾器,基本位于過濾器鍊的最底部。

public void invoke(FilterInvocation fi) throws IOException, ServletException {
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}

			InterceptorStatusToken token = super.beforeInvocation(fi);//表示檢視之前的 filter 是否通過。

			try {
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());//表示真正的調用背景的服務。
			}
			finally {
				super.finallyInvocation(token);
			}

			super.afterInvocation(token, null);
		}
	}
           

該過濾器用于控制method級别的權限控制. 官方提供了2種預設的方法權限控制寫法

一種是在方法上加注釋實作, 另一種是在configure配置中通過

//方法1, 方法定義處加注釋, 需先在具體的配置裡開啟此類配置
@Secured("ROLE_ADMIN") 
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
	
//方法2, 在配置類中複寫的configure裡直接定義
.antMatchers("your match rule").authenticated()
.antMatchers("your match rule").hasRole("ADMIN") //使用時權限會自動加字首ROLE_ADMIN
123456
           

上面兩種方法最終都會生成一個FilterSecurityInterceptor執行個體, 放在上面過濾鍊底部。 用于方法級的鑒權。

3.1.2ExceptionTranslationFilter

​ 這是一個異常過濾器,用來處理認證授權過程中抛出的異常。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		try {
			chain.doFilter(request, response);//1.調用過濾器鍊後面的filter

			logger.debug("Chain processed normally");
		}
		catch (IOException ex) {
			throw ex;
		}
		catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			RuntimeException ase = (AuthenticationException) throwableAnalyzer
					.getFirstThrowableOfType(AuthenticationException.class, causeChain);//2.如果1中的操作抛出異常,就會來到此處,判斷抛出的異常是否是AuthenticationException。
			
			if (ase == null) {	
				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
						AccessDeniedException.class, causeChain);//3.如果2不是AuthenticationException,也就是ase==null,就會判斷是否為AccessDeniedException
			}

			if (ase != null) {
				if (response.isCommitted()) {
					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
				}
				handleSpringSecurityException(request, response, chain, ase);//如果抛出的異常是AuthenticationException或者時AccessDeniedException,那麼執行此處的代碼。
			}
			else {
				// Rethrow ServletExceptions and RuntimeExceptions as-is
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				else if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}

				// Wrap other Exceptions. This shouldn't actually happen
				// as we've already covered all the possibilities for doFilter
				throw new RuntimeException(ex);
			}
		}
	}
           

通過源碼可知,該過濾器就是對認證授權中抛出的各種異常進行相應處理。

3.1.3UsernamePasswordAuthenticationFilter

該過濾器主要是對login對的POST請求做攔截,校驗表單使用者名和密碼。

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 username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

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

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

3.2兩個重要接口

UserDetailsService

UserDetailsService是SpringSecurity提供的概念模型接口,該接口用于查詢資料庫使用者名和密碼的過程。

- 建立繼承UsernamePasswordAuthenticationFilter,重寫三個方法;

- 建立類實作UserDetailService,編寫查詢資料庫過程,傳回User對象,這個User對象是安全架構提供對象。

PasswordEncoder

這是一個資料加密接口,用于傳回User對象裡面密碼加密。

3.3設定登入認證資訊

在我們進行使用者認證時,需要設定登陸時的使用者名和密碼。在SpringSecurity有三種方式可以設定:

  • 通過配置檔案
  • 通過配置類
  • 自定義編寫實作類

3.1.1通過配置檔案

可以通過在application.properties中設定我們的使用者名和密碼,如下:

#設定使用者登入的使用者名和密碼
#spring.security.user.name=admin
#spring.security.user.password=123
           

3.1.2通過配置類

通過配置類繼承WebSecurityConfigurerAdapter,重寫configure方法的方式。

package com.hexin.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * spring security配置類
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String password = passwordEncoder.encode("123");
        auth.inMemoryAuthentication().withUser("admin").password(password).roles("admin");
    }

    @Bean
    PasswordEncoder password(){
        return new BCryptPasswordEncoder();
    }
}

           

3.1.3自定義實作類

具體步驟如下:

1)建立配置類,同樣需要繼承WebSecurityConfigurerAdapter,還要設定UserDetailsService實作類。

package com.hexin.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }
}
           

2)編寫UserDetailsService實作類,用于擷取User對象,User對象有使用者名密碼和操作權限。

package com.hexin.service;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User("admin", new BCryptPasswordEncoder().encode("123"), authorities);
    }

}
           

3.4基于資料庫的使用者認證

​ 以上方式都是我們通過設定固定的使用者名和密碼來作為登陸認證的資訊,但是很多情況下使用者登入資訊是儲存到資料庫中,也就需要我們通過查詢資料庫來完成認證的過程。下面我們示範通過整合MybatisPlus來查詢資料庫完成認證。

3.4.1代碼實作

第一步:引入MabatisPlus相關依賴;

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>springsecuritylearn</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

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

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

        <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--lombok 用來簡化實體類-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
           

第二步:建立資料庫和表;

create table users(
 id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
-- 密碼 atguigu
insert into users values(1,'張
san','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
-- 密碼 atguigu
insert into users values(2,'李
si','$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na');
create table role(
id bigint primary key auto_increment,
name varchar(20)
);
insert into role values(1,'管理者');
insert into role values(2,'普通使用者');
create table role_user(
uid bigint,
rid bigint
);
insert into role_user values(1,1);
insert into role_user values(2,2);
create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)
);
insert into menu values(1,'系統管理','',0,'menu:system');
insert into menu values(2,'使用者管理','',0,'menu:user');
create table role_menu(
mid bigint,
rid bigint
);
insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);
           

第三步:配置資料庫資訊。

server.port=8001
        
#配置檔案添加資料庫配置
#mysql 資料庫連接配接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 
spring.datasource.url=jdbc:mysql://localhost:3306/springseccurity?serverTimezone=GMT%2B8 
spring.datasource.username=root 
spring.datasource.password=123456

#設定使用者登入的使用者名和密碼
#spring.security.user.name=admin
#spring.security.user.password=123
           

第四步:建立user表和對應的實體類;

package com.hexin.entity;

import lombok.Data;

@Data
public class User {
    private Integer id;
    private String username;
    private String password;
}

           

第五步:整合mapper,建立接口,繼承mapper接口;

package com.hexin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hexin.entity.User;

public interface UserMapper extends BaseMapper<User> {
}

           

第六步:在自定義實作類MyUserDetailsService調用mapper裡面的方法來查詢資料庫,然後進行認證;

package com.hexin.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hexin.entity.User;
import com.hexin.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//        return new User("admin", new BCryptPasswordEncoder().encode("123"), authorities);
        //調用userMapper方法,根據使用者名查詢資料庫中使用者資訊
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        User user = userMapper.selectOne(wrapper);
        if (user == null){
            throw new UsernameNotFoundException("使用者不存在!");
        }
        List<GrantedAuthority> grantedAuthorityList =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                new BCryptPasswordEncoder().encode(user.getPassword()), grantedAuthorityList);
    }

}
           

第七步:在啟動類上添加注解MapperScan,指定Mapper需要掃描的包;

package com.hexin;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.hexin.mapper")
@SpringBootApplication
public class SpringSecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityApplication.class, args);
    }
}
           

測試:

啟動服務,輸入使用者名密碼測試:

3.4.2添加自定義登入頁面

1)引入前端模闆依賴

<!--java模闆引擎-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
           

2)編寫前端登入頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/user/login" method="post">
        使用者名:<input type="text" name="username"/>
        <br/>
        密碼:<input type="text" name="password"/>
        <br/>
        <input type="submit" value="login"/>
    </form>
</body>
</html>
           

3)編寫控制器

package com.hexin.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("hello")
    public String Hello(){
        return "hello security!";
    }

    @GetMapping("index")
    public String index(){
        return "hello index!";
    }
}

           

4)編寫配置類

​ 編寫配置類放行登入頁面以及靜态資源。

package com.hexin.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }

    @Bean
    PasswordEncoder password() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.formLogin()    //自定義自己編寫的登入頁面
            .loginPage("/login.html")   //登入頁面配置
            .loginProcessingUrl("/user/login")  //登入通路URL
            .defaultSuccessUrl("/test/index").permitAll()   //登入成功後的跳轉路徑
            .and().authorizeRequests()
                .antMatchers("/","/test/hello","/use/login").permitAll()//設定不需要認證的路徑
            .anyRequest().authenticated()
            .and().csrf().disable();    //關閉csrf防護
    }

}
           

測試:

Spring Security入門學習Spring Security入門學習**

輸入使用者名和密碼,就可以正常登入了。

Spring Security入門學習Spring Security入門學習**

3.5基于角色或權限的通路控制

3.5.1hasAuthority方法

如果目前的使用者具有指定的權限,則傳回true,否則傳回false。

頁面表現:403,沒有通路權限。(type=Forbidden,status=403)

具體設定:

@Override
    protected void configure(HttpSecurity http) throws Exception{
        http.formLogin()    //自定義自己編寫的登入頁面
            .loginPage("/login.html")   //登入頁面配置
            .loginProcessingUrl("/user/login")  //登入通路URL
            .defaultSuccessUrl("/test/index").permitAll()   //登入成功後的跳轉路徑
            .and().authorizeRequests()
                .antMatchers("/","/test/hello","/use/login").permitAll()    //設定不需要認證的路徑
                //目前登陸的使用者隻有admins權限才可以通路
                .antMatchers("/test/index").hasAuthority("admins") //需要admins權限
                .anyRequest().authenticated()
            .and().csrf().disable();    //關閉csrf防護
    }
           

同屬需要在MyUserDetailsService.java中為使用者添加相應權限:

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//        return new User("admin", new BCryptPasswordEncoder().encode("123"), authorities);
        //調用userMapper方法,根據使用者名查詢資料庫中使用者資訊
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        User user = userMapper.selectOne(wrapper);
        if (user == null){
            throw new UsernameNotFoundException("使用者不存在!");
        }
        //為使用者添加相應的權限
        List<GrantedAuthority> grantedAuthorityList =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role,admins");//加上admins權限
        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                new BCryptPasswordEncoder().encode(user.getPassword()), grantedAuthorityList);
    }
           

測試:

Spring Security入門學習Spring Security入門學習**

可以正常通路。

如果我們不為使用者添加admins權限,就會的得到403無權限通路的錯誤。如下:

Spring Security入門學習Spring Security入門學習**

3.5.2hasAnyAuthority方法

如果目前的使用者有任何提供的角色(給定的作為一個逗号分隔的字元串清單)的話,傳回true。

具體設定:

//需要帶有admins或test中任意一個權限
.antMatchers("/test/index").hasAnyAuthority("admins,test") 
           

3.5.3hasRole方法

如果目前使用者具有指定的角色,則傳回true。

具體設定:

//需要使用者帶有角色role1
.antMatchers("/test/index").hasRole("role1")
           

同時需要設定使用者角色權限:

List<GrantedAuthority> grantedAuthorityList =
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_role1");
           

3.5.4hasAnyRole方法

辨別使用者具備熱呢一個條件都可以通路。

具體配置:

//需要使用者帶有角色role1
                .antMatchers("/test/index").hasAnyRole("role1,admin")
           

3.6基于資料庫的權限控制

​ 之前小節中,我們都是通過在UserDetailsService實作類中,手動為使用者添加角色和權限,但是在實際場景中我們都是通過查詢資料庫來擷取某個使用者的角色和權限,下面我們示範如何通過資料庫進行權限控制。

3.6.1代碼實作

1)添加實體類

添加角色類

package com.hexin.entity;

import lombok.Data;

@Data
public class Role {
    private Integer id;
    private String name;
}
           

添權重限(這裡我們用菜單辨別權限)類

package com.hexin.entity;

import lombok.Data;

@Data
public class Menu {
    private Integer id;
    private String name;
    private String url;
    private Long parentId;
    private String permission;
}
           

2)編寫持久層接口和Mapper映射檔案

UserMapper接口

package com.hexin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hexin.entity.Menu;
import com.hexin.entity.Role;
import com.hexin.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

@Mapper
public interface UserMapper extends BaseMapper<User> {

    /**
     * 根據使用者 Id 查詢使用者角色
     * @param userId
     * @return
     */
    @Select("SELECT r.id,r.name FROM role r INNER JOIN role_user ru " +
            "ON ru.rid=r.id where ru.uid=#{0}")
    List<Role> selectRoleByUserId(Integer userId);
    /**
     * 根據使用者 Id 查詢菜單
     * @param userId
     * @return
     */
    @Select("SELECT m.id,m.name,m.url,m.parentid,m.permission FROM menu m" +
            " INNER JOIN role_menu rm ON m.id=rm.mid" +
            " INNER JOIN role r ON r.id=rm.rid" +
            " INNER JOIN role_user ru ON r.id=ru.rid" +
            " WHERE ru.uid=#{0}")
    List<Menu> selectMenuByUserId(Integer userId);

}

           

3)建立UsersService接口實作類

​ 這裡需要把我們項目中原來的實作類MyUserDetailsService暫時注釋掉。建立一個UsersServiceImpl,用于查詢資料庫擷取使用者角色和權限。

UsersServiceImpl

package com.hexin.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hexin.entity.Menu;
import com.hexin.entity.Role;
import com.hexin.entity.User;
import com.hexin.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

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

@Service
public class UsersServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println("通過資料庫查詢使用者資訊。");
        //調用userMapper方法,根據使用者名查詢資料庫中使用者資訊
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        User user = userMapper.selectOne(wrapper);
        if (user == null){
            System.out.println("使用者不存在!");
            throw new UsernameNotFoundException("使用者名不存在!");
        }
        //擷取使用者角色和菜單資訊
        List<Role> roles = userMapper.selectRoleByUserId(user.getId());
        System.out.println(roles);
        List<Menu> menus = userMapper.selectMenuByUserId(user.getId());
        System.out.println(menus);
        //聲明一個集合List<GrantedAuthority>
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        //處理角色
        for (Role role : roles){
            SimpleGrantedAuthority simpleGrantedAuthority
                    = new SimpleGrantedAuthority("ROLE_"+role.getName());
            grantedAuthorityList.add(simpleGrantedAuthority);
        }
        for (Menu menu : menus){
            grantedAuthorityList.add(new SimpleGrantedAuthority(menu.getPermission()));
        }
        System.out.println(grantedAuthorityList);
        //将使用者資訊添加到目前使用者中
        return new org.springframework.security.core.userdetails.User(username,
                new BCryptPasswordEncoder().encode(user.getPassword()),grantedAuthorityList);
    }
}
           

4)添加控制器

package com.hexin.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FindController {

    @GetMapping("/findAll")
    public String findAll(){
        return "find all!";
    }

    @GetMapping("/find")
    public String index(){
        return "find!";
    }
}
           

5)修改通路配置類

.antMatchers("/findall").hasRole("管理者")
 .antMatchers("/find").hasAnyAuthority("menu:system")
           
Spring Security入門學習Spring Security入門學習**

6)使用管理者和非管理者進行測試

使用管理者:

Spring Security入門學習Spring Security入門學習**

使用非管理者:

Spring Security入門學習Spring Security入門學習**

3.6.2自定義403頁面

​ 在5.3節中我們示範類基于權限通路控制的四個方法,我們發現當使用者沒有通路權限時,會直接顯示預設的403界面給使用者,很不友好。是以本小節我們自定義我們自己的403頁面。

1)建立自定義的403頁面

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>對不起,您沒有通路權限!</h1>
    </body>
</html>
           

2)在配置類中配置

//配置沒有權限時跳轉到指定頁面(代替預設403頁面)
http.exceptionHandling().accessDeniedPage("/unauth.html");
           
Spring Security入門學習Spring Security入門學習**

再次使用無權限使用者通路測試:

Spring Security入門學習Spring Security入門學習**

3.6.3注解的使用

1)@Secured

該注解表示當使用者具有某個角色時,可以通路某方法。

使用:

首先需要在啟動類(配置類)上開啟注解。

Spring Security入門學習Spring Security入門學習**

然後在Controller的方法上使用注解,設定角色:

@GetMapping("Secured")
    @Secured("ROLE_user,ROLE_管理者")
    public String update(){
        return "Secured !";
    }
           

3、UserDetailsService實作類中設定對應的使用者角色

List<GrantedAuthority> grantedAuthorityList =
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_role,ROLE_管理者");
           

由于我們是從資料庫中擷取角色,是以確定資料庫中使用者角色存在即可。

2)@PreAuthorize

​ 該注解是在進入方法前的權限驗證,同時可以将登入使用者的 roles/permissions 參數傳到方法中。

@RequestMapping("/preAuthorize")
    @PreAuthorize("hasAnyAuthority('menu:system')")
    public String preAuthorize(){
        System.out.println("preAuthorize");
        return "preAuthorize";
    }
           

3)@PostAuthorize

該注解注解使用并不多,在方法執行後再進行權限驗證,适合驗證帶有傳回值的權限。

@RequestMapping("/testPostAuthorize")
    @PostAuthorize("hasAnyAuthority('menu:system')")
    public String PostAuthorize(){
        System.out.println("test--PostAuthorize");
        return "PostAuthorize"; 
    }
}
           

4)PostFilter

​ 該注解的作用是對方法傳回的資料進行過濾。

@RequestMapping("getAll")
    @PreAuthorize("hasRole('ROLE_管理者')")
    @PostFilter("filterObject.username == 'admin1'") //在校驗之後進行過濾,留下使用者admin1傳回給前端
    @ResponseBody
    public List<User> getAllUser() {
        ArrayList<User> list = new ArrayList<>();
        list.add(new User(1, "admin1", "6666"));
        list.add(new User(2, "admin2", "888"));
        return list;
    }
           

5)PreFilter

​ 該注解的作用是對傳入的資料進行過濾。

@RequestMapping("getTestPreFilter")
    @PreAuthorize("hasRole('ROLE_管理者')")
    @PreFilter(value = "filterObject.id%2==0")  //過濾使用者id為偶數
    @ResponseBody
    public List<User> getTestPreFilter(@RequestBody List<User> list){
        list.forEach(t-> {
            System.out.println(t.getId()+"\t"+t.getUsername());
        });
        return list;
    }
           

3.7使用者登出

​ 1)實作使用者登出需要再配置類中添加退出的配置,如下:

//配置使用者登出
http.logout()
    .logoutUrl("/logout")   //使用者登出的url
    .logoutSuccessUrl("/test/hello").permitAll();
           
Spring Security入門學習Spring Security入門學習**

​ 2)建立使用者登入成功頁面并添加登出按鈕

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        登入成功<br>
        <a href="/logout">退出</a>
    </body>
</html>
           

3)我們還需要修改一個配置,将我們登入成功後跳轉到success.html頁面

http.formLogin()    //自定義自己編寫的登入頁面
    .loginPage("/login.html")   //登入頁面配置
    .loginProcessingUrl("/user/login")  //登入通路URL
    .defaultSuccessUrl("/success.html").permitAll()   //登入成功後的跳轉路徑
           
Spring Security入門學習Spring Security入門學習**

測試:

​ 首先登入;

Spring Security入門學習Spring Security入門學習**

​ 然後點選退出,并通路其他controller。

Spring Security入門學習Spring Security入門學習**

發現一家不能正常通路了。

3.8基于資料庫實作記住我

3.8.1實作原理

​ “記住我”的功能主要是依賴浏覽器的cookie技術,下面我們用一張圖簡單說明一下原理:

Spring Security入門學習Spring Security入門學習**

1、用戶端發出認證請求,服務端UsernamePasswordAuthenticationFilter過濾器攔截到請求并進行認證處理;

2、認證成功後,會進入RemeberMeService處理業務;

3、然後将Token寫入浏覽器的Cookie中,同時将Token資訊寫入到資料庫中進行儲存;

4、當使用者之後的請求會被RememberMeAuthenticationFilter過濾器攔截,擷取Cookie中的Token資訊并與資料看比對,比對成功就會進行後續處理。

3.8.2具體實作

1)建立表persistent_logins

該表用于儲存使用者登入後的token及過期時間等資訊

CREATE TABLE `persistent_logins` (
 `username` varchar(64) NOT NULL,
 `series` varchar(64) NOT NULL,
 `token` varchar(64) NOT NULL,
 `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
CURRENT_TIMESTAMP,
 PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
           

2)修改配置類

@Autowired
    private DataSource dataSource;//注入資料源

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){//配置PersistentTokenRepository對象
        JdbcTokenRepositoryImpl jdbcTokenRepository = new
                JdbcTokenRepositoryImpl();
        // 指派資料源
        jdbcTokenRepository.setDataSource(dataSource);
        // 自動建立表,第一次執行會建立,以後要執行就要删除掉!
        //jdbcTokenRepository.setCreateTableOnStartup(true);//這裡我們是自己建立的表就不需要自動建立了
        return jdbcTokenRepository;
    }
           
Spring Security入門學習Spring Security入門學習**

3)修改配置中的自動登入

.and().rememberMe().tokenRepository(persistentTokenRepository())  //設定自動登入
                  .tokenValiditySeconds(60) //有效時長60s
                  .userDetailsService(userDetailsService)
           
Spring Security入門學習Spring Security入門學習**

4)頁面添加記住我複選框

PS:此處name 屬性值必須位 remember-me。不能改為其他值

Spring Security入門學習Spring Security入門學習**

測試:

先登入成功,然後關閉浏覽器,重新打開發現依然可以通路。打開資料庫也能看到一條記錄。

Spring Security入門學習Spring Security入門學習**

繼續閱讀