鉴于整个项目非常庞大,所以本项目将拆分成几篇文章来详细讲解。这篇文章是开篇,将使用mysql数据库,Druid连接池,JPA框架来搭建一个基础的用户权限系统。
原本还想写个理论篇的,介绍JWT和SpringSecurity的认证机制,但是网上关于这方面的教程较多,就不班门弄斧了。下面贴出几个理论文章,建议弄懂理论部分在来看本系列。
10分钟了解JSON Web令牌(JWT)
SpringSecurity登录原理(源码级讲解)
代码地址:gitee
/*
用户表
*/
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_’前缀)
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
com.shiep
jwtauth
0.0.1-SNAPSHOT
jwtauth
Demo project for JWT Auth
1.8
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
com.alibaba
druid
1.1.8
log4j
log4j
1.2.17
com.alibaba
fastjson
1.2.36
io.jsonwebtoken
jjwt
0.9.0
org.springframework.boot
spring-boot-starter-thymeleaf
2.0.6.RELEASE
org.springframework.boot
spring-boot-maven-plugin
上面是完整项目的pom依赖,有些本章用不到,不过可以先导入。
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
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 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;
}
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 {
/**
* 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 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 {
}
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 {
/**
* 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注解是用于增、删、改。
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 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: 加密工具,我是在下一章的SpringSecurity中将其配置为Bean的,如果需要测试使用,可以在程序主类中先将其配置为Bean
*
* @param null
* @return
*/
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public FXUser findByUserName(String username) {
return userRepository.findByName(username);
}
@Override
public List 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 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表。
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 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监控配置。
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单点登录。
下一章:JWT+SpringSecurity实现基于Token的单点登录(二):认证和授权