【SpringSecurity系列01】初識SpringSecurity

​ 什麼是SpringSecurity

​ Spring Security是一個能夠爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減少了爲企業系統安全控制編寫大量重複代碼的工作。

以上來介紹來自wiki,比較官方。

​ 用自己的話 簡單介紹一下,Spring Security基於 Servlet 過濾器鏈的形式,爲我們的web項目提供認證授權服務。它來自於Spring,那麼它與SpringBoot整合開發有着天然的優勢,目前與SpringSecurity對應的開源框架還有shiro。接下來我將通過一個簡單的例子帶大家來認識SpringSecurity,然後通過分析它的源碼帶大家來認識一下SpringSecurity是如何工作,從一個簡單例子入門,大家由淺入深的瞭解學習SpringSecurity

通常大家在做一個後臺管理的系統的時候,應該採用session判斷用戶是否登錄。我記得我在沒有接觸學習SpringSecurity與shiro之前。對於用戶登錄功能實現通常是如下:

public String login(User user, HttpSession session){
  //1、根據用戶名或者id從數據庫讀取數據庫中用戶
  //2、判斷密碼是否一致
  	//3、如果密碼一致
  		session.setAttribute("user",user);
  	//4、否則返回登錄頁面
  
}

對於之後那些需要登錄之後才能訪問的url,通過SpringMvc的攔截器中的#preHandle來判斷session中是否有user對象
如果沒有 則返回登錄頁面
如果有, 則允許訪問這個頁面。

但是在SpringSecurity中,這一些邏輯已經被封裝起來,我們只需要簡單的配置一下就能使用。

接下來我通過一個簡單例子大家認識一下SpringSecurity

本文基於SpringBoot,如果大家對SpringBoot不熟悉的話可以看看我之前寫的SpringBoot入門系列

我使用的是:

  • SpringBoot 2.1.4.RELEASE
  • SpringSecurity 5
<?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>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.yukong</groupId>
	<artifactId>springboot-springsecurity</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springboot-springsecurity</name>
	<description>springboot-springsecurity study</description>

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

	<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.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.0.1</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</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>

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

</project>

配置一下數據庫 以及MyBatis

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/db_test?useUnicode=true&characterEncoding=utf8
    password: abc123
mybatis:
  mapper-locations: classpath:mapper/*.xml

這裏我用的MySQL8.0 大家注意一下 MySQL8.0的數據庫驅動的類的包改名了

在前面我有講過SpringBoot中如何整合Mybatis,在這裏我就不累述,有需要的話看這篇文章

user.sql

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `username` varchar(32) NOT NULL COMMENT '用戶名',
  `svc_num` varchar(32) DEFAULT NULL COMMENT '用戶號碼',
  `password` varchar(100) DEFAULT NULL COMMENT '密碼',
  `cust_id` bigint(20) DEFAULT NULL COMMENT '客戶id  1對1',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

對應的UserMapper.java

package com.yukong.mapper;

import com.yukong.entity.User;

/**
 *
 * @author yukong
 * @date 2019-04-11 16:50
 */
public interface UserMapper {

    int insertSelective(User record);

    User selectByUsername(String  username);


}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yukong.mapper.UserMapper">
  <resultMap id="BaseResultMap" type="com.yukong.entity.User">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="username" jdbcType="VARCHAR" property="username" />
    <result column="svc_num" jdbcType="VARCHAR" property="svcNum" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="cust_id" jdbcType="BIGINT" property="custId" />
  </resultMap>
  <sql id="Base_Column_List">
    id, username, svc_num, `password`, cust_id
  </sql>
  <select id="selectByUsername" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from user
    where username = #{username,jdbcType=VARCHAR}
  </select>

  <insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.yukong.entity.User" useGeneratedKeys="true">
    insert into user
    <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="username != null">
        username,
      </if>
      <if test="svcNum != null">
        svc_num,
      </if>
      <if test="password != null">
        `password`,
      </if>
      <if test="custId != null">
        cust_id,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="username != null">
        #{username,jdbcType=VARCHAR},
      </if>
      <if test="svcNum != null">
        #{svcNum,jdbcType=VARCHAR},
      </if>
      <if test="password != null">
        #{password,jdbcType=VARCHAR},
      </if>
      <if test="custId != null">
        #{custId,jdbcType=BIGINT},
      </if>
    </trim>
  </insert>
</mapper>

在這裏我們定義了兩個方法。

國際慣例ctrl+shift+t創建mapper的測試方法,並且插入一條記錄

package com.yukong.mapper;

import com.yukong.SpringbootSpringsecurityApplicationTests;
import com.yukong.entity.User;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.*;

/**
 * @author yukong
 * @date 2019-04-11 16:53
 */

public class UserMapperTest extends SpringbootSpringsecurityApplicationTests {


    @Autowired
    private UserMapper userMapper;


    @Test
    public void insert() {
        User user = new User();
        user.setUsername("yukong");
        user.setPassword("abc123");
        userMapper.insertSelective(user);
    }

}

運行測試方法,並且成功插入一條記錄。

創建UserController.java

package com.yukong.controller;

import com.yukong.entity.User;
import com.yukong.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author yukong
 * @date 2019-04-11 15:22
 */
@RestController
public class UserController {
    
    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/user/{username}")
    public User hello(@PathVariable String username) {
        return userMapper.selectByUsername(username);
    }

}

這個方法就是根據用戶名去數據庫查找用戶詳細信息。

啓動。因爲我們之前插入過一條username=yukong的記錄,所以我們查詢一下,訪問127.0.0.1:8080/user/yukong

[圖片上傳失敗…(image-ea02ac-1554981869345)]

我們可以看到 我們被重定向到了一個登錄界面,這也是我們之前引入的spring-boot-security-starter起作用了。

大家可能想問了,用戶名跟密碼是什麼,用戶名默認是user,密碼在啓動的時候已經通過日誌打印在控制檯了。
image-20190411171141031.png

現在我們輸入用戶跟密碼並且登錄。就可以成功訪問我們想要訪問的接口。

image-20190411171540312

從這裏我們可以知道,我只需要引入了Spring-Security的依賴,它就開始生效,並且保護我們的接口了,但是現在有一個問題就是,它的用戶名只能是user並且密碼是通過日誌打印在控制檯,但是我們希望它能通過數據來訪問我們的用戶並且判斷登錄。

其實想實現這個功能也很簡單。這裏我們需要了解兩個接口。

  • UserDetails
  • UserDetailsService

UserDetails

所以,我們需要將我們的User.java實現這個接口

package com.yukong.entity;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 *
 * @author yukong
 * @date 2019-04-11 16:50
 */
public class User implements UserDetails {
    /**
    * 主鍵
    */
    private Long id;

    /**
    * 用戶名
    */
    private String username;

    /**
    * 用戶號碼
    */
    private String svcNum;

    /**
    * 密碼
    */
    private String password;

    /**
    * 客戶id  1對1
    */
    private Long custId;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getSvcNum() {
        return svcNum;
    }

    public void setSvcNum(String svcNum) {
        this.svcNum = svcNum;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 這裏我們沒有用到權限,所以返回一個默認的admin權限
        return AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Long getCustId() {
        return custId;
    }

    public void setCustId(Long custId) {
        this.custId = custId;
    }
}

接下來我們再看看UserDetailsService

image

它只有一個方法的聲明,就是通過用戶名去查找用戶信息,從這裏我們應該知道了,SpringSecurity回調UserDetails#loadUserByUsername去獲取用戶,但是它不知道用戶信息存在哪裏,所以定義成接口,讓使用者去實現。在我們這個項目用 我們的用戶是存在了數據庫中,所以我們需要調用UserMapper的方法去訪問數據庫查詢用戶信息。這裏我們新建一個類叫MyUserDetailsServiceImpl

package com.yukong.config;

import com.yukong.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * @author yukong
 * @date 2019-04-11 17:35
 */
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userMapper.selectByUsername(username);
    }
}

然後新建一個類去把我們的UserDetailsService配置進去

這裏我們新建一個SecurityConfig

package com.yukong.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;

/**
 * @author yukong
 * @date 2019-04-11 15:08
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;


    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        // 配置UserDetailsService 跟 PasswordEncoder 加密器
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        auth.eraseCredentials(false);
    }
}

在這裏我們還配置了一個PasswordEncoder加密我們的密碼,大家都知道密碼明文存數據庫是很不安全的。

接下里我們插入一條記錄,需要注意的是 密碼需要加密。

package com.yukong.mapper;

import com.yukong.SpringbootSpringsecurityApplicationTests;
import com.yukong.entity.User;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.Assert.*;

/**
 * @author yukong
 * @date 2019-04-11 16:53
 */

public class UserMapperTest extends SpringbootSpringsecurityApplicationTests {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserMapper userMapper;


    @Test
    public void insert() {
        User user = new User();
        user.setUsername("yukong");
        user.setPassword(passwordEncoder.encode("abc123"));
        userMapper.insertSelective(user);
    }

}

接下來啓動程序,並且登錄,這次只需要輸入插入到數據中的那條記錄的用戶名跟密碼即可。

在這裏一節中,我們瞭解到如何使用springsecurity 完成一個登錄功能,接下我們將通過分析源碼來了解爲什麼需要這個配置,以及SpringSecurity的工作原理是什麼。

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