前言
鑑於整個項目非常龐大,所以本項目將拆分成幾篇文章來詳細講解。這篇文章是開篇,將使用mysql數據庫,Druid連接池,JPA框架來搭建一個基礎的用戶權限系統。
原本還想寫個理論篇的,介紹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單點登錄。