JWT+SpringSecurity实现基于Token的单点登录(一):前期准备

前言

        鉴于整个项目非常庞大,所以本项目将拆分成几篇文章来详细讲解。这篇文章是开篇,将使用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_’前缀

二、pom.xml依赖导入



    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依赖,有些本章用不到,不过可以先导入。

三、配置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 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 {
    /**
     * 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注解是用于增、删、改。

六、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 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表。

七、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 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的依赖先删除

JWT+SpringSecurity实现基于Token的单点登录(一):前期准备_第1张图片

发送后的返回结果:

发现已经注册成功。接着查看用户信息。

JWT+SpringSecurity实现基于Token的单点登录(一):前期准备_第2张图片

到这里基础配置已经完毕。下面我在讲下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

JWT+SpringSecurity实现基于Token的单点登录(一):前期准备_第3张图片

登录后我们就可以查看数据库状态了。

JWT+SpringSecurity实现基于Token的单点登录(一):前期准备_第4张图片

十、后记

上面配置是关于用户模块的基础配置,下一章将讲解如何从数据库加载用户和角色信息进行认证和鉴权。 登录时生成用户Token,之后访问只需携带Token进行访问即可,实现sso单点登录。

下一章:JWT+SpringSecurity实现基于Token的单点登录(二):认证和授权

 

 

 

你可能感兴趣的:(Spring,JWT)