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

2 篇文章 1 订阅

前言

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

<?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: 加密工具,我是在下一章的SpringSecurity中将其配置为Bean的,如果需要测试使用,可以在程序主类中先将其配置为Bean
     *
     * @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单点登录。

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

 

 

 

  • 3
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
JWT是一种跨越不同系统和应用程序的安全认证协议,主要由三个组成部分组成:头部、载荷和签名。Spring Security是一个Java框架,用于提供身份验证和授权的安全性管理。在本篇文章中,我们将讨论使用Spring Security实现JWT身份验证的过程。 第一步是配置Spring Security,我们需要创建一个Spring Security配置类。在配置类中,我们需要定义安全性规则,如哪些角色可以访问哪些URL,验证用户信息的服务等。 第二步是添加JWT依赖包,以便我们能够使用JWT机制来创建、验证和解析Token。我们可以使用不同的Java JWT库来生成JWT Token。通过这些库,可以从负载参数中构建Token,可以利用不同的算法来加密Token生成签名,可以解析Token得到有效负载信息等。 第三步是在Spring Security中,添加一个过滤器用于进行JWT的身份验证,这个过滤器会在请求中提取出JWT信息进行验证。如果验证通过,就会进行授权并将请求传递给下一个过滤器。如果无法验证或验证失败,则返回错误响应或跳转到安全页面。 第四步是在应用程序中,生成并返回JWT Token,将其添加到HTTP的Authorization头部中。当请求到达服务器时,Spring Security中的过滤器会解析Authorization头部,提取JWT Token,并将其传递给JWT身份验证过滤器进行验证。 最后,我们需要对外部用户暴露JWT生成和解析的功能。在这种情况下,需要提供API接口,使外部用户能够获取JWT Token并进行下一步操作。 总的来说,使用Spring Security实现JWT身份验证可以提供一个快速、安全的身份验证机制。使用JWT机制可以生成安全且无状态的Token,可以方便地跨不同应用程序和分布式系统的认证。同时,Spring Security提供了非常便捷的配置工具和API,使我们可以更加快速地实现和管理安全规则。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值