JWT+SpringSecurity實現基於Token的單點登錄(一):前期準備

前言

        鑑於整個項目非常龐大,所以本項目將拆分成幾篇文章來詳細講解。這篇文章是開篇,將使用mysql數據庫,Druid連接池,JPA框架來搭建一個基礎的用戶權限系統。

  原本還想寫個理論篇的,介紹JWT和SpringSecurity的認證機制,但是網上關於這方面的教程較多,就不班門弄斧了。下面貼出幾個理論文章,建議弄懂理論部分在來看本系列。

10分鐘瞭解JSON Web令牌(JWT)

SpringSecurity登錄原理(源碼級講解)

 一、數據庫搭建

/*
用戶表
 */
create table FX_USER(
  USER_ID integer not null primary key auto_increment,
  USER_NAME varchar(50) not null,
  USER_PASSWORD varchar(100) not null
);
/*
通過用戶名登錄,用戶名設置成唯一,相當於用戶賬戶
 */
ALTER TABLE `fx_user` ADD UNIQUE( `USER_NAME`);


/*
角色表
 */
create table FX_ROLE(
    ROLE_ID integer not null primary key,
    ROLE_NAME varchar(50) not null
);
/*
角色名唯一約束
 */
ALTER TABLE `fx_role` ADD UNIQUE( `ROLE_NAME`);


/*
角色用戶映射表
 */
create table FX_USER_ROLE(
    USER_ID integer not null,
    ROLE_ID integer not null,
    foreign key(USER_ID) references fx_user(USER_ID),
    foreign key(ROLE_ID) references fx_role(ROLE_ID),
    primary key(USER_ID,ROLE_ID)
);

上面創建了三個表,role表用於存放系統中的角色,user表用於存放用戶帳號密碼,而user_role表是用戶的角色映射。

然後往role表中填入初始數據。

/*
角色表初始數據
 */
insert into FX_ROLE values (1,"ROLE_USER");
insert into FX_ROLE values (2,"ROLE_ADMIN");

默認系統角色有兩種:user和admin。(ROLE_NAME字段加上‘ROLE_’前綴是因爲SpringSecurity的角色默認包含‘ROLE_’前綴

二、pom.xml依賴導入

<?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.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.shiep</groupId>
    <artifactId>jwtauth</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jwtauth</name>
    <description>Demo project for JWT Auth</description>

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

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <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>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>

        <!-- druid數據庫連接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.8</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <!-- Druid依賴log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.36</version>
        </dependency>
        <!--JWT-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

        <!-- 使用thymeleaf視圖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.0.6.RELEASE</version>
        </dependency>
    </dependencies>

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

</project>

上面是完整項目的pom依賴,有些本章用不到,不過可以先導入。

三、配置application.yml

spring:
  # 配置thymeleaf視圖
  resources:
    static-locations: classpath:/templates/
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    mode: HTML
    servlet:
      content-type: text/html
    cache: false

  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/jwtauth?characterEncoding=utf-8&useSSl=false&serverTimezone=GMT%2B8
    schema: classpath:schema.sql
    data: classpath:data.sql
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 配置druid數據連接池
    type: com.alibaba.druid.pool.DruidDataSource
    # 監控統計攔截的filters
    filters: stat,wall,log4j
    # 連接池的初始大小、最小、最大
    initialSize: 5
    minIdle: 5
    maxActive: 20
    # 獲取連接的超時時間
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    # 一個連接在池中最小生存的時間
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: false
    maxPoolPreparedStatementPerConnectionSize: 20
    connectionProperties:
      druid:
        stat:
          mergeSql: true
          slowSqlMillis: 5000

  jpa:
    generate-ddl: false
    show-sql: true
    hibernate:
      ddl-auto: update
    open-in-view: false

 

四、搭建實體entity層

package com.shiep.jwtauth.entity;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 14:38
 * @description: 數據庫中FX_USER表的實體類
 */
@Data
@Entity
@Table(name = "FX_USER")
public class FXUser implements Serializable {
    private static final long serialVersionUID = 4517281710313312135L;

    @Id
    @Column(name = "USER_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY) //id自增長
    private Integer id;

    @Column(name = "USER_NAME",nullable = false)
    private String name;

    @Column(name = "USER_PASSWORD",nullable = false)
    private String password;

    /**
     * @Transient 表明是臨時字段,roles是該用戶的角色列表
     */
    @Transient
    private List<String> roles;
}

@Data註解是Lombok這個插件提供的,可以自動生成getter、setter等方法。

roles字段用於之後存放該用戶的角色列表。

package com.shiep.jwtauth.entity;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 15:31
 * @description: 映射數據庫中的FX_ROLE角色表
 */
@Data
@Entity
@Table(name = "FX_ROLE")
public class FXRole implements Serializable {
    private static final long serialVersionUID = -3112666718610962186L;

    @Id
    @Column(name = "ROLE_ID")
    @GeneratedValue(strategy = GenerationType.IDENTITY) //id自增長
    private Integer id;

    @Column(name = "ROLE_NAME",nullable = false)
    private String name;
}
package com.shiep.jwtauth.entity;

import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 16:53
 * @description: 數據庫中FX_USER_ROLE表的實體類
 */
@Data
@Entity
@Table(name = "FX_USER_ROLE")
@IdClass(FXUserRole.class)
public class FXUserRole implements Serializable {
    private static final long serialVersionUID = 6746672328835480737L;
    @Id
    @Column(name = "USER_ID",nullable = false)
    private Integer userId;

    @Id
    @Column(name = "ROLE_ID",nullable = false)
    private Integer roleId;
}

五、搭建dao層

package com.shiep.jwtauth.repository;

import com.shiep.jwtauth.entity.FXUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 14:59
 * @description: FXUser的dao層
 */

@Repository
public interface FXUserRepository extends JpaRepository<FXUser,Integer> {
    /**
     * description: 通過UserName查找User
     *
     * @param userName
     * @return com.shiep.jwtauth.entity.FXUser
     */
    FXUser findByName(String userName);

    /**
     * description: 通過UserName查找該用戶的角色列表
     *
     * @param userName
     * @return java.lang.String
     */
    @Query(nativeQuery = true,value ="SELECT ROLE_NAME from fx_role WHERE ROLE_ID in (select ROLE_ID from fx_user_role where USER_ID = (select USER_ID from fx_user where USER_NAME= ?1));")
    List<String> getRolesByUserName(String userName);


}
FXUserRepository繼承了JpaRepository,然後在類中聲明瞭兩個方法,其中findByName將通過用戶名來查找這個用戶,而getRolesByUserName方法使用@Query註解來定製自己的sql語句,nativeQuery = true表示使用sql語句。
package com.shiep.jwtauth.repository;

import com.shiep.jwtauth.entity.FXRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 16:49
 * @description:  FXRole的dao層
 */
@Repository
public interface FXRoleRepository extends JpaRepository<FXRole,Integer> {

}
package com.shiep.jwtauth.repository;

import com.shiep.jwtauth.entity.FXUserRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 17:05
 * @description: FXUserRole的dao層
 */
@Repository
@Transactional(rollbackFor = Exception.class)
public interface FXUserRoleRepository extends JpaRepository<FXUserRole,FXUserRole> {
    /**
     * description: 根據用戶名和角色名保存用戶角色表
     *
     * @param userName
     * @param roleName
     * @return void
     */
    @Modifying
    @Query(nativeQuery = true,value = "INSERT INTO fx_user_role VALUES((SELECT USER_ID from fx_user where USER_NAME=?1),(SELECT ROLE_ID FROM fx_role WHERE ROLE_NAME=?2));")
    void save(String userName,String roleName);
}
@Transactional(rollbackFor = Exception.class)註解表示啓用事務,類中定義了save方法,用於新增用戶權限,@Modifying註解是用於增、刪、改。

六、Service層

package com.shiep.jwtauth.service;

import com.shiep.jwtauth.entity.FXUser;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 15:00
 * @description: FXUser的Service接口
 */
@Transactional(rollbackFor = Exception.class)
public interface IFXUserService {
    /**
     * description: 通過用戶名查找用戶
     *
     * @param username
     * @return com.shiep.jwtauth.entity.FXUser
     */
    FXUser findByUserName(String username);

    /**
     * description: 通過用戶名得到角色列表
     *
     * @param userName
     * @return java.lang.String
     */
    List<String> getRolesByUserName(String userName);

    /**
     * description: 通過用戶名密碼創建用戶,默認角色爲ROLE_USER
     *
     * @param userName
     * @param password
     * @return com.shiep.jwtauth.entity.FXUser
     */
    FXUser insert(String userName,String password);
}
IFXUserService接口中定義了三個方法,具體註釋中已經解釋清楚了。下面看看它的實現類。
package com.shiep.jwtauth.service.impl;

import com.shiep.jwtauth.entity.FXUser;
import com.shiep.jwtauth.repository.FXUserRepository;
import com.shiep.jwtauth.repository.FXUserRoleRepository;
import com.shiep.jwtauth.service.IFXUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

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

/**
 * @author: 倪明輝
 * @date: 2019/3/6 15:01
 * @description: IFXUserService的實現類
 */
@Service
public class FXUserServiceImpl implements IFXUserService {

    @Autowired
    FXUserRepository userRepository;

    @Autowired
    private FXUserRoleRepository userRoleRepository;

    /**
     * description: 加密工具
     *
     * @param null
     * @return
     */
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public FXUser findByUserName(String username) {
        return userRepository.findByName(username);
    }

    @Override
    public List<String> getRolesByUserName(String userName) {
        return userRepository.getRolesByUserName(userName);
    }

    @Override
    public FXUser insert(String userName, String password) {
        FXUser user = new FXUser();
        user.setName(userName);
        // 將密碼加密後存入數據庫
        user.setPassword(bCryptPasswordEncoder.encode(password));
        List<String> roles = new ArrayList<>();
        roles.add("ROLE_USER");
        user.setRoles(roles);
        // 將用戶信息存入FX_USER表中
        FXUser result = userRepository.save(user);
        if (result.getName()!=null){
            // 插入用戶成功時生成用戶的角色信息
            userRoleRepository.save(result.getName(),"ROLE_USER");
            result.setRoles(roles);
            return result;
        }
        return null;
    }


}

這裏主要講解下insert方法。用戶註冊邏輯:首先將用戶密碼加密,然後將UserName和加密後的password存入數據庫。接着,採用默認的權限user,將用戶權限存入user_role表。

七、Controller控制層

package com.shiep.jwtauth.controller;

import com.shiep.jwtauth.entity.FXUser;
import com.shiep.jwtauth.service.IFXUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 15:05
 * @description:
 */
@RestController
@RequestMapping(path = "/user",produces = "application/json;charset=utf-8")
public class FXUserController {
    @Autowired
    IFXUserService userService;

    @GetMapping("/{userName}")
    public FXUser getUser(@PathVariable String userName){
        FXUser user = userService.findByUserName(userName);
        user.setRoles(userService.getRolesByUserName(userName));
        return user;
    }
}

FXUserController寫了一個方法,用來讀取用戶信息及用戶角色信息,但是我們此時還沒有用戶,因此在寫個控制層來註冊用戶。

package com.shiep.jwtauth.controller;

import com.shiep.jwtauth.entity.FXUser;
import com.shiep.jwtauth.service.IFXUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * @author: 倪明輝
 * @date: 2019/3/6 16:30
 * @description: 控制層
 */
@RestController
@RequestMapping(path = "/auth",produces = "application/json;charset=utf-8")
public class AuthController {

    @Autowired
    private IFXUserService userService;

    /**
     * description: 註冊默認權限(ROLE_USER)用戶
     *
     * @param registerUser
     * @return java.lang.String
     */
    @PostMapping("/register")
    public String registerUser(@RequestBody Map<String,String> registerUser){
        String userName=registerUser.get("username");
        String password=registerUser.get("password");
        FXUser user=userService.insert(userName,password);
        if(user==null){
            return "新建用戶失敗";
        }
        return user.toString();
    }
}

八、測試

首先,我們使用postman來發送請求註冊用戶。(如果測試有認證問題,請將SpringSecurity的依賴先刪除

發送後的返回結果:

發現已經註冊成功。接着查看用戶信息。

到這裏基礎配置已經完畢。下面我在講下Druid監控配置。

九、Druid監控配置

package com.shiep.jwtauth.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import javax.sql.DataSource;

/**
 * @author: 倪明輝
 * @date: 2019/3/7 16:48
 * @description: Druid連接池配置
 */

@Configuration
@PropertySource(value = "classpath:application.yml")
public class DruidConfig {

    /**
     * description: 配置數據域
     *
     * @param
     * @return javax.sql.DataSource
     */
    @Bean(destroyMethod = "close", initMethod = "init")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }

    /**
     * description: 註冊一個StatViewServlet
     *
     * @param
     * @return org.springframework.boot.web.servlet.ServletRegistrationBean
     */
    @Bean
    public ServletRegistrationBean druidStatViewServlet(){
        //通過ServletRegistrationBean類進行註冊.
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");

        //添加初始化參數:initParams
        //白名單:
        servletRegistrationBean.addInitParameter("allow","127.0.0.1");
        //IP黑名單 (存在共同時,deny優先於allow) : 如果滿足deny的話提示:Sorry, you are not permitted to view this page.
        //servletRegistrationBean.addInitParameter("deny","192.168.1.73");
        //登錄查看信息的賬號密碼.
        servletRegistrationBean.addInitParameter("loginUsername","admin");
        servletRegistrationBean.addInitParameter("loginPassword","123456");
        //是否能夠重置數據.
        servletRegistrationBean.addInitParameter("resetEnable","false");
        return servletRegistrationBean;
    }

    /**
     * description: druid過濾器,註冊一個filterRegistrationBean
     *
     * @param
     * @return org.springframework.boot.web.servlet.FilterRegistrationBean
     */
    @Bean
    public FilterRegistrationBean druidStatFilter(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
        //添加過濾規則.
        filterRegistrationBean.addUrlPatterns("/*");
        //添加不需要忽略的格式信息.
        filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }

}

配置好後,訪問http://localhost:8080/druid/login.html,賬號密碼爲上面代碼設置的admin,123456

登錄後我們就可以查看數據庫狀態了。

十、後記

上面配置是關於用戶模塊的基礎配置,下一章將講解如何從數據庫加載用戶和角色信息進行認證和鑑權。 登錄時生成用戶Token,之後訪問只需攜帶Token進行訪問即可,實現sso單點登錄。

 

 

 

 

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