转载

Spring Security(三)

SpringSecurity整合SpringBoot集中式版本

技术选型

  • SpringBoot 2.1.3
  • SpringSecurity
  • MySQL 5.7
  • Mybatis
  • JSP

初步整合认证第一版

创建工程并导入jar包

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

编写控制器

package com.weiwei.xu.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/product")
public class ProductController {

    @RequestMapping(value = "/findAll")
    @ResponseBody
    public String findAll(){
        return "success";
    }

}

编写启动类

package com.weiwei.xu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootSecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSecurityApplication.class, args);
    }
}

测试

  • 输入: http://localhost :8080/product/findAll。

Spring Security(三)

加入SpringSecurity的jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

重启测试

  • SpringBoot已经为SpringSecurity提供了默认配置,默认所有的资源都必须认证通过之后才能访问。

Spring Security(三)

  • SpringBoot已经提供了默认的用户名user,密码在项目启动的时候随机生成,如下图所示:

Spring Security(三)

  • 认证通过之后就可以继续访问处理器资源:

Spring Security(三)

项目地址

  • 初步整合认证第一版地址

初步整合认证第二版

说明

  • SpringBoot官方是不推荐在SpringBoot中使用JSP的,但是SpringBoot是可以使用JSP作为模板引擎的,本项目采用JSP作为前端展示页面。

导入SpringBoot的tomcat启动插件jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

加入JSP页面等静态资源

  • 如果在src/main目录下没有webapp目录,那么就创建webapp目录,并且手动将Java工程改为web工程。

Spring Security(三)

  • 如果webapp目录,和下图的类似,就可以用了。

Spring Security(三)

  • 导入静态资源,注意WEB-INF就不用了。

    Spring Security(三)

提供SpringSecurity的配置类

package com.weiwei.xu.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * SpringSecurity的配置类
 *
 * @author weiwei.xu
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {


    /**
     * 认证用户的来源[内存或数据库]
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("{noop}123456").roles("USER");
    }


    /**
     * SpringSecurity相关信息
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置
        http.authorizeRequests().antMatchers("/login.jsp","/failer.jsp","/css/**","/img/**","/pages/**").permitAll()
                .antMatchers("/**").access("hasAnyRole('USER','ADMIN')")
                .anyRequest()
                .authenticated()
                .and()
                .formLogin().loginPage("/login.jsp").loginProcessingUrl("/login").successForwardUrl("/index.jsp").failureForwardUrl("/failer.jsp").permitAll()
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login.jsp")
                .invalidateHttpSession(true) //清空session
                .permitAll();
    }
}

修改启动类

package com.weiwei.xu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

/**
 * 启动类
 *
 * @author weiwei.xu
 */
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
public class SpringBootSecurityApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSecurityApplication.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SpringBootSecurityApplication.class);
    }


}

配置视图解析器

  • application.yml
server:
  port: 8080

# 配置视图解析器
spring:
  mvc:
    view:
      prefix: /pages/
      suffix: .jsp

测试

自定义认证页面

Spring Security(三)

认证成功页面

Spring Security(三)

项目地址

  • 初步整合认证第二版地址

初步整合认证第三版(数据库认证)

准备工作

/*
 Navicat Premium Data Transfer

 Source Server         : 192.168.1.57
 Source Server Type    : MySQL
 Source Server Version : 50729
 Source Host           : 192.168.1.57:33060
 Source Schema         : security

 Target Server Type    : MySQL
 Target Server Version : 50729
 File Encoding         : 65001

 Date: 26/04/2020 11:18:36
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for persistent_logins
-- ----------------------------
DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins`  (
  `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `series` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `token` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `last_used` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
  PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of persistent_logins
-- ----------------------------
INSERT INTO `persistent_logins` VALUES ('xiaoming', '93OEeplI7zAKwc0BqVnW+Q==', 'e4JvptA5h8fR+FFgmJSL9g==', '2020-04-24 10:23:57');
INSERT INTO `persistent_logins` VALUES ('xiaoming', 'CmWGQ/rmb7lXAx7dHiEVGw==', 'OvsaKtaC046whNL56Y4MqA==', '2020-04-24 10:30:29');
INSERT INTO `persistent_logins` VALUES ('xiaoming', 'JJqJ5vJ/7IqMLxj/JpLDRg==', 'vIfFHeD6fYSV8oCHMdty7g==', '2020-04-24 10:42:48');
INSERT INTO `persistent_logins` VALUES ('xiaoming', 'T9NMx6AiG5J3RNSM841juw==', 'v67JBqXg7AkhLtH++LISNw==', '2020-04-24 10:35:57');

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `permission_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单名称',
  `permission_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜单地址',
  `parent_id` int(11) NOT NULL DEFAULT 0 COMMENT '父菜单id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `role_name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名称',
  `role_desc` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (6, 'ROLE_USER', '普通用户');
INSERT INTO `sys_role` VALUES (7, 'ROLE_ADMIN', '系统管理员');

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `rid` int(11) NOT NULL COMMENT '角色编号',
  `pid` int(11) NOT NULL COMMENT '权限编号',
  PRIMARY KEY (`rid`, `pid`) USING BTREE,
  INDEX `FK_Reference_12`(`pid`) USING BTREE,
  CONSTRAINT `FK_Reference_11` FOREIGN KEY (`rid`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FK_Reference_12` FOREIGN KEY (`pid`) REFERENCES `sys_permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名称',
  `password` varchar(120) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
  `status` int(1) NULL DEFAULT 1 COMMENT '1开启0关闭',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (4, 'admin', '$2a$10$ynlaufZM048G5jsp98seeuvkAXNCVD5RFEudlrW.xiNihU.2Tjm9W', 1);
INSERT INTO `sys_user` VALUES (5, 'xiaoming', '$2a$10$ynlaufZM048G5jsp98seeuvkAXNCVD5RFEudlrW.xiNihU.2Tjm9W', 1);

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `uid` int(11) NOT NULL COMMENT '用户编号',
  `rid` int(11) NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`uid`, `rid`) USING BTREE,
  INDEX `FK_Reference_10`(`rid`) USING BTREE,
  CONSTRAINT `FK_Reference_10` FOREIGN KEY (`rid`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FK_Reference_9` FOREIGN KEY (`uid`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (5, 6);
INSERT INTO `sys_user_role` VALUES (4, 7);

SET FOREIGN_KEY_CHECKS = 1;

导入数据库操作相关的jar包

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

在配置文件中添加数据库操作相关配置

  • application.yml
server:
  port: 8080

# 配置视图解析器
spring:
  mvc:
    view:
      prefix: /pages/
      suffix: .jsp
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.1.57:33060/security
    username: root
    password: 123456

mybatis:
  type-aliases-package: com.weiwei.xu.domain
  mapper-locations:  classpath:/com/weiwei/xu/mapper/*.mapper.xml

编写实体类

  • SysUser.java
package com.weiwei.xu.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * 系统用户
 *
 * @author weiwei.xu
 */
public class SysUser implements UserDetails {

    private Integer id;

    private String username;

    private String password;

    private Integer status;

    private Set<SysRole> roles = new HashSet<>();

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Set<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(Set<SysRole> roles) {
        this.roles = roles;
    }

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles;
    }

}
  • SysRole.java
package com.weiwei.xu.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;

/**
 * 系统角色
 *
 * @author weiwei.xu
 */
public class SysRole implements GrantedAuthority {

    private Integer id;

    private String roleName;

    private String roleDesc;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getRoleDesc() {
        return roleDesc;
    }

    public void setRoleDesc(String roleDesc) {
        this.roleDesc = roleDesc;
    }

    @JsonIgnore
    @Override
    public String getAuthority() {
        return roleName;
    }
}

编写Mapper接口

  • SysUserMapper.java
package com.weiwei.xu.mapper;

import com.weiwei.xu.domain.SysUser;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;

import java.util.Set;

public interface SysUserMapper {

    @Select("select * from sys_user where username = #{username} ")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "roles", column = "id", javaType = Set.class,
                    many = @Many(select = "com.weiwei.xu.mapper.SysRoleMapper.findByUid"))
    })
    SysUser findByUsername(String username);

}
  • SysRoleMapper.java
package com.weiwei.xu.mapper;

import com.weiwei.xu.domain.SysRole;
import org.apache.ibatis.annotations.Select;

import java.util.Set;

public interface SysRoleMapper {

    /**
     * 根据uid查询用户所对应的角色
     *
     * @param uid
     * @return
     */
    @Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc " +
            "FROM sys_role r, sys_user_role ur " +
            "WHERE r.id=ur.rid AND ur.uid=#{uid}")
    Set<SysRole> findByUid(Integer uid);

}

编写业务层接口和实现类

  • SysUserService.java
package com.weiwei.xu.service;

import org.springframework.security.core.userdetails.UserDetailsService;

public interface SysUserService extends UserDetailsService {
}
  • SysUserServiceImpl.java
package com.weiwei.xu.service.impl;

import com.weiwei.xu.mapper.SysUserMapper;
import com.weiwei.xu.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return sysUserMapper.findByUsername(username);
    }
}

修改SpringSecurity的配置类

package com.weiwei.xu.config;

import com.weiwei.xu.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * SpringSecurity的配置类
 *
 * @author weiwei.xu
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SysUserService sysUserService;

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证用户的来源[内存或数据库]
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(sysUserService).passwordEncoder(passwordEncoder());
    }


    /**
     * SpringSecurity相关信息
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //释放静态资源,指定资源拦截规则,指定自定义认证页面,指定退出认证配置,csrf配置
        http.authorizeRequests().antMatchers("/login.jsp", "/failer.jsp", "/css/**", "/img/**", "/pages/**").permitAll()
                .antMatchers("/**").access("hasAnyRole('USER','ADMIN')")
                .anyRequest()
                .authenticated()
                .and()
                .formLogin().loginPage("/login.jsp").loginProcessingUrl("/login").successForwardUrl("/index.jsp").failureForwardUrl("/failer.jsp").permitAll()
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login.jsp")
                .invalidateHttpSession(true) //清空session
                .permitAll();
    }
}
  • 在启动类上开启方法级的授权注解:
package com.weiwei.xu;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

/**
 * 启动类
 *
 * @author weiwei.xu
 */
@MapperScan("com.weiwei.xu.mapper")
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringBootSecurityApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSecurityApplication.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SpringBootSecurityApplication.class);
    }
}
  • 在产品处理器上添加注解:
package com.weiwei.xu.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping(value = "/product")
public class ProductController {

    @RequestMapping(value = "/findAll")
    @Secured({"ROLE_PRODUCT","ROLE_ADMIN"})
    public String findAll(){
        return "product-list";
    }

}

处理403异常和其它异常

package com.weiwei.xu.exception;

import org.springframework.security.acls.model.NotFoundException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.nio.file.AccessDeniedException;

@ControllerAdvice
public class SecurityExceptionHandler {

    @ExceptionHandler(value = AccessDeniedException.class)
    public String handle403Exception(){
        return "redirect:403";
    }

    @ExceptionHandler(value = RuntimeException.class)
    public String handle500Exception(){
        return "redirect:500";
    }

    @ExceptionHandler(value = NotFoundException.class)
    public String handle404Exception(){
        return "redirect:404";
    }
}

项目地址

  • 初步整合认证第三版

SpringSecurity整合SpringBoot分布式版本

分布式认证概念说明

  • 分布式认证,即我们常说的单点登录,简称SSO,指的是在多应用系统的项目中,用户只需要登录一次,就可以访问所有互相信任的应用系统。

分布式认证流程图

  • 在分布式项目中,每台服务器都有各自独立的session,而这些session之间是无法直接共享资源的,所以,session通常不能作为单点登录的解决方案。
  • 最合理的单点登录方案流程如下图所示:

Spring Security(三)

  • 总结,单点登录要实现的两大环节:

    • ①用户认证:用户向认证服务器发起认证请求,认证服务器给用户返回一个成功的令牌token,主要在认证服务器中完成,认证服务器只有一个。
    • ②身份校验:用户携带token去访问其他服务器时,在其他服务器中药对token的真伪进行校验,主要在资源服务器中完成,资源服务器可以有多个。

JWT介绍

概念说明

  • 从分布式认证流程中,我不难发现,这中间起最关键作用的就是token,token的安全与否,直接关系到系统的健壮性,这里我们选择使用jwt来实现token的生成和校验。
  • jwt,全程JSON Web Token, 官网地址 ,是一款出色的分布式身份校验方案。可以生成token,也可以解析校验token。
  • JWT生成的token由以下三部分组成:

    • 头部:主要设置一些规范信息,签名部分的编码格式就在头部中声明。
    • 载荷:token中存放有效信息的部分,比如用户名、用户角色、过期时间等,但是不要放密码,会泄露!
    • 签名:将头部和载荷分别采用base64编码后,用“.”相连,再加入盐,最后使用头部声明的编码类型进行编码,就得到了签名。

JWT生成的token的安全性分析

  • 从JWT生成的token组成上来看,要想避免token被伪造,主要就看签名部分了,而签名部分又有三部分组成,其中头部和载荷的base编码,几乎是透明的,毫无安全性可言,那么最终守护token安全的重担就落在了加入盐上面了。
  • 如果生成token所用的盐和解析token所加入的盐是一样的,那后果将不堪设想,大家都可以使用这个盐来解析token,用来伪造token。
  • 这时,就需要对盐采用非对称加密的方式进行加密,以达到生成token和校验token时所用的盐不一致的安全效果。

非对称机密RSA介绍

  • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐藏保存,公钥可以发给信任客户端。

    • 私钥加密,持有私钥或公钥才可以解密。
    • 公钥加密,持有私钥才可以解密。
  • 优点:安全,难以破解。
  • 缺点:算法比较耗时,为了安全,可以接受。
  • 历史:三位数学家Rivest、Shamir和Adleman设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA。

SpringSecurity+JWT+RSA分布式认证思路分析

SpringSecurity主要是通过过滤器来实现功能的,所以我们需要找到SpringSecurity实现认证和校验身份的过滤器。

回顾集中式认证流程

  • 用户认证:

    • 使用UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法实现认证功能,该过滤器父类中的successfulAuthentication方法实现认证成功后的操作。
  • 身份校验:

    • 使用BasicAuthenticationFilter过滤器中的doFilterInternal方法验证是否登录,以决定能否进入后续过滤器。

分布式认证流程

  • 用户认证:

    • 由于,分布式项目,多数是前后端分离的架构设计,我们要满足可以接受异步POST的认证请求参数,需要修改UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法,让其能够接受请求体。另外,默认的successfulAuthentication方法在认证通过后,是把用户信息直接放入到session中就完事了,现在我们需要修改这个方法,在认证通过后生成token并返回给用户。
  • 身份认证:

    • 原来BasicAuthenticationFilter过滤器中的doFilterInternal方法校验用户是否登录,就是看session中是否有用户信息,我们需要修改为,验证用户携带的token是否合法,并解析出用户信息,交给SpringSecurity,以便后续的授权功能可以正常使用。

SpringSecurity+JWT+RSA分布式实现

导入相应的jar包

<properties>
        <java.version>1.8</java.version>
        <!--   swagger   -->
        <io.springfox.version>2.9.2</io.springfox.version>
        <io.swagger.version>1.5.21</io.swagger.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.7</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.4</version>
        </dependency>
        <!--   mysql     -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
        <!-- Swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${io.springfox.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>io.swagger</groupId>
                    <artifactId>swagger-annotations</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.swagger</groupId>
                    <artifactId>swagger-models</artifactId>
                </exclusion>
                <exclusion>
                    <artifactId>mapstruct</artifactId>
                    <groupId>org.mapstruct</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${io.springfox.version}</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>${io.swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>${io.swagger.version}</version>
        </dependency>
    </dependencies>

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

工具类

  • RsaUtils.java
package com.weiwei.xu.utils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

/**
 * RSA算法
 *
 * @author weiwei.xu
 */
public class RsaUtils {

    private static final int DEFAULT_KEY_SIZE = 2048;

    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    private static PublicKey getPublicKey(byte[] bytes) throws Exception {
        bytes = Base64.getDecoder().decode(bytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        bytes = Base64.getDecoder().decode(bytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret, int keySize) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(Math.max(keySize, DEFAULT_KEY_SIZE), secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
        File dest = new File(destPath);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        if (!dest.exists()) {
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}
  • JwtUtils.java
package com.weiwei.xu.utils;


import cn.hutool.json.JSONUtil;
import com.weiwei.xu.common.Payload;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.security.PrivateKey;
import java.security.PublicKey;
import java.sql.Date;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.UUID;

/**
 * 生成token以及校验token相关方法
 *
 * @author: weiwei.xu
 */
public class JwtUtils {

    private static final String JWT_PAYLOAD_USER_KEY = "user";

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位分钟
     * @return JWT
     */
    public static String generateTokenExpireInMinutes(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JSONUtil.parseObj(userInfo).toStringPretty())
                .setId(createJTI())
                .setExpiration(Date.from(LocalDateTime.now().plusMinutes(expire).atZone(ZoneId.systemDefault()).toInstant()))
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 私钥加密token
     *
     * @param userInfo   载荷中的数据
     * @param privateKey 私钥
     * @param expire     过期时间,单位秒
     * @return JWT
     */
    public static String generateTokenExpireInSeconds(Object userInfo, PrivateKey privateKey, int expire) {
        return Jwts.builder()
                .claim(JWT_PAYLOAD_USER_KEY, JSONUtil.parseObj(userInfo).toStringPretty())
                .setId(createJTI())
                .setExpiration(Date.from(LocalDateTime.now().plusSeconds(expire).atZone(ZoneId.systemDefault()).toInstant()))
                .signWith(privateKey, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥
     * @return Jws<Claims>
     */
    private static Jws<Claims> parserToken(String token, PublicKey publicKey) {
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    private static String createJTI() {
        return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setUserInfo(JSONUtil.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
        claims.setExpiration(body.getExpiration());
        return claims;
    }

    /**
     * 获取token中的载荷信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     */
    public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        Payload<T> claims = new Payload<>();
        claims.setId(body.getId());
        claims.setExpiration(body.getExpiration());
        return claims;
    }
}
  • Payload.java
package com.weiwei.xu.common;

import lombok.*;

import java.util.Date;

/**
 * @author weiwei.xu
 * 为了方便后期获取token中的用户信息,将token中载荷部分单独封装成一个对象
 */
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public class Payload<T> {
    private String id;
    private T userInfo;
    private Date expiration;
}

SpringBoot的配置文件

  • application.yml
server:
  port: 8080
  servlet:
    context-path: /security

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.1.57:33060/security?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
    username: root
    password: 123456


mybatis:
  mapper-locations: classpath:com/weiwei/xu/mapper/*.mapper.xml
  type-aliases-package: com.weiwei.xu.domain
  configuration:
    map-underscore-to-camel-case: true

rsa:
  key:
    pubKeyFile: D:/auth_key/id_key_rsa.pub
    priKeyFile: D:/auth_key/id_key_rsa

配置类

  • RsaKeyProperties.java
package com.weiwei.xu.config;

import com.weiwei.xu.utils.RsaUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
 * @author weiwei.xu
 */
@Configuration
@ConfigurationProperties(value = "rsa.key")
public class RsaKeyProperties {

    private String pubKeyFile;

    private String priKeyFile;

    private PublicKey publicKey;

    private PrivateKey privateKey;

    public String getPubKeyFile() {
        return pubKeyFile;
    }

    public void setPubKeyFile(String pubKeyFile) {
        this.pubKeyFile = pubKeyFile;
    }

    public String getPriKeyFile() {
        return priKeyFile;
    }

    public void setPriKeyFile(String priKeyFile) {
        this.priKeyFile = priKeyFile;
    }

    public PublicKey getPublicKey() {
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    public PrivateKey getPrivateKey() {
        return privateKey;
    }

    public void setPrivateKey(PrivateKey privateKey) {
        this.privateKey = privateKey;
    }

    @PostConstruct
    public void createResKey() throws Exception {
        publicKey = RsaUtils.getPublicKey(pubKeyFile);
        privateKey = RsaUtils.getPrivateKey(priKeyFile);
    }
}
  • SpringSecurityConfig.java
package com.weiwei.xu.config;

import com.weiwei.xu.filter.JwtTokenFilter;
import com.weiwei.xu.filter.JwtVerifyFilter;
import com.weiwei.xu.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * SpringSecurity的配置类
 *
 * @author weiwei.xu
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    private SysUserService sysUserService;

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private RsaKeyProperties rsaKeyProperties;


    /**
     * 配置认证来源
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(sysUserService).passwordEncoder(passwordEncoder());
    }

    /**
     * 配置过滤请求
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http.   //关闭跨域站点请求
                cors().and().csrf().disable().authorizeRequests()
                // swagger 文档
                .antMatchers("/swagger-ui.html/**").permitAll()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/webjars/**").permitAll()
                .antMatchers("/*/api-docs").permitAll()
                // 文件
                .antMatchers("/avatar/**").permitAll()
                .antMatchers("/file/**").permitAll()
                // 阿里巴巴 druid
                .antMatchers("/druid/**").permitAll()
                .anyRequest().authenticated()
                .and()
                //自定义认证过滤器
                .addFilter(new JwtTokenFilter(authenticationManager(), rsaKeyProperties))
                //自定义URL过滤器
                .addFilter(new JwtVerifyFilter(authenticationManager(), rsaKeyProperties))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        ;
    }
}
  • SwaggerConfig.java
package com.weiwei.xu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

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

/**
 * SwaggerConfig
 *
 * @author weiwei.xu
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    static List<Parameter> parameters;

    static {
        /**
         * 这是为了我们在用 swagger 测试接口的时候添加头部信息
         */
        parameters = new ArrayList<>();
        ParameterBuilder tokenPar = new ParameterBuilder();
        tokenPar.name("Authorization").description("swagger测试用(模拟token传入)非必填 header").modelRef(new ModelRef("string")).parameterType("header").required(false);
        parameters.add(tokenPar.build());
    }

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
//                .apis(RequestHandlerSelectors.basePackage("com.weiwei.xu.controller.ProductController"))
                .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
                .paths(PathSelectors.ant("/**"))
                .build()
                .globalOperationParameters(parameters)
                .enable(true);
    }


    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("SpringBoot整合SpringSecurity分布式版本")
                .termsOfServiceUrl("")
                .version("1.0")
                .build();
    }
}

实体类

  • SysUser.java
package com.weiwei.xu.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.*;

/**
 * 系统用户
 *
 * @author weiwei.xu
 */
public class SysUser implements UserDetails {

    private Integer id;

    private String username;

    private String password;

    private Integer status;

    private List<SysRole> roles = new ArrayList<>();

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public List<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles;
    }

}
  • SysRole.java
package com.weiwei.xu.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;

/**
 * 系统角色
 *
 * @author weiwei.xu
 */
public class SysRole implements GrantedAuthority {

    private Integer id;

    private String roleName;

    private String roleDesc;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getRoleDesc() {
        return roleDesc;
    }

    public void setRoleDesc(String roleDesc) {
        this.roleDesc = roleDesc;
    }

    @JsonIgnore
    @Override
    public String getAuthority() {
        return roleName;
    }
}

Mapper接口

  • SysUserMapper.java
package com.weiwei.xu.mapper;

import com.weiwei.xu.domain.SysUser;
import org.apache.ibatis.annotations.*;

import java.util.List;
@Mapper
public interface SysUserMapper {

    @Select("select * from sys_user where username = #{username} ")
    @Results({
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "roles", column = "id", javaType = List.class,
                    many = @Many(select = "com.weiwei.xu.mapper.SysRoleMapper.findByUid"))
    })
    SysUser findByUsername(String username);

}
  • SysRoleMapper.java
package com.weiwei.xu.mapper;

import com.weiwei.xu.domain.SysRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.Set;
@Mapper
public interface SysRoleMapper {

    /**
     * 根据uid查询用户所对应的角色
     *
     * @param uid
     * @return
     */
    @Select("SELECT r.id, r.role_name roleName, r.role_desc roleDesc " +
            "FROM sys_role r, sys_user_role ur " +
            "WHERE r.id=ur.rid AND ur.uid=#{uid}")
    Set<SysRole> findByUid(Integer uid);

}

Service层接口及实现类

  • SysUserService.java
package com.weiwei.xu.service;

import org.springframework.security.core.userdetails.UserDetailsService;

public interface SysUserService extends UserDetailsService {
}
  • SysUserServiceImpl.java
package com.weiwei.xu.service.impl;

import com.weiwei.xu.mapper.SysUserMapper;
import com.weiwei.xu.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class SysUserServiceImpl implements SysUserService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return sysUserMapper.findByUsername(username);
    }
}

控制器

  • ProductController.java
package com.weiwei.xu.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@RestController
@RequestMapping(value = "/product")
public class ProductController {

    @GetMapping(value = "/findAll")
    @Secured({"ROLE_PRODUCT"})
    public Map<String, Object> findAll() {
        Map<String, Object> map = new ConcurrentHashMap<>();
        map.put("aa", "bb");
        return map;
    }

}

JWT相关的过滤器

  • JwtTokenFilter.java
package com.weiwei.xu.filter;


import com.fasterxml.jackson.databind.ObjectMapper;
import com.weiwei.xu.config.RsaKeyProperties;
import com.weiwei.xu.domain.SysRole;
import com.weiwei.xu.domain.SysUser;
import com.weiwei.xu.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * JWT的token Filter
 *
 * @author weiwei.xu
 */
public class JwtTokenFilter extends UsernamePasswordAuthenticationFilter {

    private RsaKeyProperties rsaKeyProperties;

    private AuthenticationManager authenticationManager;

    public JwtTokenFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        this.rsaKeyProperties = rsaKeyProperties;
        this.authenticationManager = authenticationManager;
    }

    /**
     * 试图认证的方法
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        try {
            SysUser sysUser = new ObjectMapper().readValue(request.getInputStream(), SysUser.class);

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());

            return authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        } catch (Exception e) {
            try {
                //如果认证失败,提供自定义的json异常信息
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                PrintWriter writer = response.getWriter();
                Map<String, Object> resultMap = new ConcurrentHashMap<>();
                resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
                resultMap.put("msg", "用户名或密码错误");
                writer.write(new ObjectMapper().writeValueAsString(resultMap));
                writer.flush();
                writer.close();
            } catch (Exception exception) {
                exception.printStackTrace();
            }
            return null;
        }


    }


    /**
     * 认证成功之后,生成token,返回给前端
     *
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        String name = authResult.getName();
        SysUser sysUser = new SysUser();
        sysUser.setUsername(name);

        sysUser.setRoles((List<SysRole>) authResult.getAuthorities());

        String token = JwtUtils.generateTokenExpireInMinutes(sysUser, rsaKeyProperties.getPrivateKey(), 24 * 60);

        //向客户端响应token
        response.addHeader("Authorization", "Bearer " + token);

        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        PrintWriter writer = response.getWriter();
        Map<String, Object> resultMap = new ConcurrentHashMap<>();
        resultMap.put("code", HttpServletResponse.SC_OK);
        resultMap.put("msg", "登录成功");
        writer.write(new ObjectMapper().writeValueAsString(resultMap));
        writer.flush();
        writer.close();
    }
}
  • JwtVerifyFilter.java
package com.weiwei.xu.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.weiwei.xu.common.Payload;
import com.weiwei.xu.config.RsaKeyProperties;
import com.weiwei.xu.domain.SysUser;
import com.weiwei.xu.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author weiwei.xu
 */
public class JwtVerifyFilter extends BasicAuthenticationFilter {

    private RsaKeyProperties rsaKeyProperties;

    public JwtVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties rsaKeyProperties) {
        super(authenticationManager);
        this.rsaKeyProperties = rsaKeyProperties;
    }


    /**
     * 过滤请求
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
//            try {
//                response.setContentType("application/json;charset=utf-8");
//                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//                PrintWriter writer = response.getWriter();
//                Map<String, Object> resultMap = new ConcurrentHashMap<>();
//                resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
//                resultMap.put("msg", "请登录");
//                writer.write(new ObjectMapper().writeValueAsString(resultMap));
//                writer.flush();
//                writer.close();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }

            return;
        } else {
            String token = header.replace("Bearer ", "");
            Payload<SysUser> sysUserPayload = null;
            try {
                sysUserPayload = JwtUtils.getInfoFromToken(token, rsaKeyProperties.getPublicKey(), SysUser.class);
            } catch (Exception e) {

                try {
                    //如果认证失败,提供自定义的json异常信息
                    response.setContentType("application/json;charset=utf-8");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    PrintWriter writer = response.getWriter();
                    Map<String, Object> resultMap = new ConcurrentHashMap<>();
                    resultMap.put("code", HttpServletResponse.SC_FORBIDDEN);
                    resultMap.put("msg", "token解析错误");
                    writer.write(new ObjectMapper().writeValueAsString(resultMap));
                    writer.flush();
                    writer.close();
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            }


            SysUser sysUser = sysUserPayload.getUserInfo();

            if (null != sysUser) {

                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword(), sysUser.getAuthorities());

                SecurityContextHolder.getContext().setAuthentication(authentication);
                chain.doFilter(request, response);
            }
        }

    }
}

项目地址

  • SpringSecurity整合SpringBoot分布式版本
原文  https://segmentfault.com/a/1190000022662889
正文到此结束
Loading...