千锋教育SpringSecurity框架教程,Spring Security+JWT微服务项目实战教程

https://www.bilibili.com/video/BV1Fd4y1k7rq/?p=3&spm_id_from=pageDriver&vd_source=0d7b1712ce42c1a2fa54bb4e1d601d78

 

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>


作者:千锋-索尔

版本:QF1.0

版权:千锋Java教研院

 

一、Spring Security简介

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。Spring Security致力于为Java应用程序提供身份验证和授权的能力。像所有Spring项目一样,Spring Security的真正强大之处在于它可以轻松扩展以满足定制需求的能力。

角色和权限时许

Spring Security两大重要核心功能:用户认证(Authentication)用户授权(Authorization)

  • 用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

  • 用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,有的用户既能读取,又能修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

 

二、快速开始

使用Springboot工程搭建Spring Security项目。

1.引入依赖

在pom中新增了Spring Security的依赖

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

 

2.创建测试访问接口

用于访问接口时触发Spring Security登陆页面

package com.qf.my.ss.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* web controller
* @author Thor
* @公众号 Java架构栈
*/
@RestController
public class SecurityController {

   @RequestMapping("/hello")
   public String hello(){
       return "hello security";
  }
}

 

3.访问接口,自动跳转至Security登陆页面

访问add接口,讲自动跳转至Security的登陆页面

image-20210305105043157

 

默认账号是: user

默认密码是:启动项目的控制台中输出的密码

快速开始

 

三、Spring Security基础概念

在上一节中访问add接口,发现被Spring Security的登陆页面拦截,可以猜到这是触发了Security框架的过滤器。Spring Security本质上就是一个过滤器链。下面讲介绍Security框架的过滤器链。

 

1.过滤器链

  • WebAsyncManagerIntegrationFilter:将SecurityContext集成到Spring MVC中用于管理异步请求处理的WebAsyncManager中。

  • SecurityContextPersistenceFilter:在当前会话中填充SecurityContext,SecurityContext即Security的上下文对象,里面包含了当前用户的认证及权限信息等。

  • HeaderWriterFilter:向请求的Header中添加信息

  • CsrfFilter:用于防止CSRF(跨域请求伪造)攻击。Spring Security会对所有post请求验证是否包含系统生成的CSRF的信息,如果不包含则报错。

  • LogoutFilter:匹配URL为“/logout”的请求,清除认证信息,实现用户注销功能。

  • UsernamePasswordAuthenticationFilter:认证操作的过滤器,用于匹配URL为“/login”的POST请求做拦截,校验表单中的用户名和密码。

  • DefaultLoginPageGeneratingFilter:如果没有配置登陆页面,则生成默认的认证页面

  • DefaultLogoutPageGeneratingFilter:用于生成默认的退出页面

  • BasicAuthenticationFilter:用于Http基本认证,自动解析Http请求头中名为Authentication的内容,并获得内容中“basic”开头之后的信息。

  • RequestCacheAwareFilter:用于缓存HttpServletRequest

  • SecurityContextHolderAwareRequestFilter:用于封装ServletRequest,让ServletRequest具备更多功能。

  • AnonymousAuthenticationFilter:对于未登录情况下的处理,当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中

  • SessionManagementFilter:限制同一用户开启多个会话

  • ExceptionTranslationFilter:异常过滤器,用来处理在认证授权过程中抛出异常。

  • FilterSecurityInterceptor:获取授权信息,根据SecurityContextHolder中存储的用户信息判断用户是否有权限访问

 

2.过滤器加载过程

Springboot在整合Spring Security项目时会自动配置DelegatingFilterProxy过滤器,若非Springboot工程,则需要手动配置该过滤器。

 

springsecurity过滤器链

 

过滤器如何进行加载的?

结合上图和源码,Security在DelegatingFilterProxy的doFilter()调用了initDelegat()方法,在该方法中调用了WebApplicationContext的getBean()方法,该方法出发FilterChainProxy的doFilterInternal方法,用于获取过滤链中的所有过滤器并进行加载。

 

四、Spring Security的认证方式-基本认证

1.认证概念

所谓的认证,就是用来判断系统中是否存在某用户,并判断该用户的身份是否合法的过程,解决的其实是用户登录的问题。认证的存在,是为了保护系统中的隐私数据与资源,只有合法的用户才可以访问系统中的资源。

2.认证方式

在Spring Security中,常见的认证方式可以分为HTTP层面和表单层面,常见的认证方式如下:

  • HTTP基本认证

  • Form表单认证

  • HTTP摘要认证

3.基本认证

HTTP基本认证是在RFC2616标准中定义的一种认证模式,它以一种很简单的方式与用户进行交互。HTTP基本认证可以分为如下4个步骤:

  • 客户端首先发起一个未携带认证信息的请求;

  • 然后服务器端返回一个401 Unauthorized的响应信息,并在WWW-Authentication头部中说明认证形式:当进行HTTP基本认证时,WWW-Authentication会被设置为Basic realm=“被保护的页面”;

  • 接下来客户端会收到这个401 Unauthorized响应信息,并弹出一个对话框,询问用户名和密码。当用户输入后,客户端会将用户名和密码使用冒号进行拼接并用Base64编码,然后将其放入到请求的Authorization头部并发送给服务器;

  • 最后服务器端对客户端发来的信息进行解码得到用户名和密码,并对该信息进行校验判断是否正确,最终给客户端返回响应内容。

HTTP基本认证是一种无状态的认证方式,与表单认证相比,HTTP基本认证是一种基于HTTP层面的认证方式,无法携带Session信息,也就无法实现Remember-Me功能。另外,用户名和密码在传递时仅做了一次简单的Base64编码,几乎等同于以明文传输,极易被进行密码窃听和重放攻击。所以在实际开发中,很少会使用这种认证方式来进行安全校验。

基本认证的代码实现:

  • 创建SecurityConfig配置类

package com.qf.my.ss.demo.config;

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;

/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //1.配置基本认证方式
       http.authorizeRequests()
              .anyRequest()
              .authenticated()
              .and()
               //开启basic认证
              .httpBasic();
  }
}
  • Basic认证详解

在未登录状态下访问目标资源时,查看响应头,可以看到WWW-Authenticate认证信息:WWW-Authenticate:Basic realm="Realm",其中WWW-Authenticate: 表示服务器告知浏览器进行代理认证工作。Basic: 表示认证类型为Basic认证。realm="Realm": 表示认证域名为Realm域。

根据401和以上响应头信息,浏览器会弹出一个对话框,要求输入 用户名/密码,Basic认证会将其拼接成 “用户名:密码” 格式,中间是一个冒号,并利用Base64编码成加密字符串xxx;然后在请求头中附加 Authorization: Basic xxx 信息,发送给后台认证;后台需要利用Base64来进行解码xxx,得到用户名和密码,再校验 用户名:密码 信息。如果认证错误,浏览器会保持弹框;如果认证成功,浏览器会缓存有效的Base64编码,在之后的请求中,浏览器都会在请求头中添加该有效编码。

五、Form表单认证

在SpringBoot开发环境中,只要我们添加了Spring Security的依赖包,就会自动实现表单认证。可以通过WebSecurityConfigurerAdapter提供的configure方法看到默认的认证方式就是表单认证

    protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}

1.表单认证中的预置url和页面

默认的formLogin配置中,自动配置了一些url和页面:

  • /login(get): get请求时会跳转到这个页面,只要我们访问任意一个需要认证的请求时,都会跳转到这个登录界面。

  • /login(post): post请求时会触发这个接口,在登录页面点击登录时,默认的登录页面表单中的action就是关联这个login接口。

  • /login?error: 当用户名或密码错误时,会跳转到该页面。

  • /: 登录成功后,默认跳转到该页面,如果配置了index.html页面,则 ”/“ 会重定向到index.html页面,当然这个页面要由我们自己实现。

  • /logout: 注销页面。

  • /login?logout: 注销成功后跳转到的页面。

由此可见,SpringSecurity默认有两个login,即登录页面和登录接口的地址都是 /login:

如果是 GET 请求,表示你想访问登录页面;如果是 POST 请求,表示你想提交登录数据。 对于这几个URL接口,我们简单了解即可。

2.自定义认证页面

  • 自定义登陆页面


<!doctype html>
<html lang="zh-CN">
<head>
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
   <title>Bootstrap 101 Template</title>

   <!-- Bootstrap -->
   <link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
   <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
   <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
   <!--[if lt IE 9]>
   <script src="https://cdn.jsdelivr.cn/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
   <script src="https://cdn.jsdelivr.cn/npm/respond.js@1.4.2/dest/respond.min.js"></script>
   <![endif]-->
   <style>
       .login-top{
           width: 600px;
           height: 300px;
           border: 1px solid #DCDFE6;
           margin: 150px auto;
           padding: 20px 50px 20px 30px;
           border-radius: 20px;
           box-shadow: 0px 0px 20px #DCDFE6;
      }
   </style>
</head>
<body>
<div class="login-top">
   <div>
       <h3>欢迎登陆</h3>
   </div>

   <form action="/login" method="post">

       <div class="form-group" style="padding-bottom: 20px">
           <label for="inputUsername" class="col-sm-2 control-label">用户名</label>
           <div class="col-sm-10">
               <input type="text" class="form-control" id="inputUsername" name="username" placeholder="用户名">
           </div>
       </div>

       <div class="form-group">
           <label for="inputPassword" class="col-sm-2 control-label">密码</label>
           <div class="col-sm-10">
               <input type="password" class="form-control" id="inputPassword" name="password" placeholder="密码">
           </div>
       </div>


       <div class="form-group">
           <div class="col-sm-offset-2 col-sm-10">
               <div class="checkbox">
                   <label>
                       <input type="checkbox"> 记住我
                   </label>
               </div>
           </div>
       </div>
       <div class="form-group">
           <div class="col-sm-offset-2 col-sm-10">
               <button type="submit" class="btn btn-default">登陆</button>
           </div>
       </div>


   </form>

</div>
</body>
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script><!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.2/css/bootstrap-utilities.min.css" rel="stylesheet"></body>
</html>
  • 自定义首页

  • 自定义错误页面

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
   <h3>用户名或密码错误</h3>
</body>
</html>

3.自定义配置项

package com.qf.my.ss.demo.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


   @Override
   public void configure(WebSecurity web) throws Exception {
       web.ignoring()
              .antMatchers("/js/**","/css/**","/images/**");
  }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //1.配置基本认证方式
       http.authorizeRequests()
              .anyRequest()
              .authenticated()
              .and()
              .formLogin()
              .loginPage("/login.html")
              .permitAll()
//指登录成功后,是否始终跳转到登录成功url。它默认为false
              .defaultSuccessUrl("/index.html",true)
//post登录接口,登录验证由系统实现
              .loginProcessingUrl("/login")
//用户密码错误跳转接口
              .failureUrl("/error.html")
               //要认证的用户参数名,默认username
              .usernameParameter("username")
//要认证的密码参数名,默认password
              .passwordParameter("password")
              .and()
//配置注销
              .logout()
//注销接口
              .logoutUrl("/logout")
//注销成功后跳转到的接口
              .logoutSuccessUrl("/login.html")
              .permitAll()
//删除自定义的cookie
              .deleteCookies("myCookie")
              .and()
//注意:需禁用crsf防护功能,否则登录不成功
              .csrf()
              .disable();
  }
}

 

4.WebSecurity和HttpSecurity

Spring Security内部是如何加载我们自定义的登录页面的?需要了解这两个类:WebSecurity和HttpSecurity。

  • WebSecurity

在这个类里定义了一个securityFilterChainBuilders集合,可以同时管理多个SecurityFilterChain过滤器链,

当WebSecurity在执行时,会构建出一个名为 ”springSecurityFilterChain“Spring BeanFilterChainProxy代理类,它的作用是来 定义哪些请求可以忽略安全控制,哪些请求必须接受安全控制;以及在合适的时候 清除SecurityContext 以避免内存泄漏,同时也可以用来 定义请求防火墙和请求拒绝处理器,也可以在这里 开启Spring Security 的Debug模式

  • HttpSecurity

HttpSecurity用来构建包含一系列的过滤器链SecurityFilterChain,平常我们的配置就是围绕着这个SecurityFilterChain进行。

5.Http摘要认证

  • 概念

HTTP摘要认证和HTTP基本认证一样,也是在RFC2616中定义的一种认证方式,它的出现是为了弥补HTTP基本认证存在的安全隐患,但该认证方式也并不是很安全。HTTP摘要认证会使用对通信双方来说都可知的口令进行校验,且最终以密文的形式来传输数据,所以相对于基本认证来说,稍微安全了一些

HTTP摘要认证与基本认证类似,基于简单的“挑战-回应”模型。当我们发起一个未经认证的请求时,服务器会返回一个401回应,并给客户端返回与验证相关的参数,期待客户端依据这些参数继续做出回应,从而完成整个验证过程。

  • 摘要认证核心参数

服务端给客户端返回的验证相关参数如下:

username: 用户名。

password: 用户密码。

realm: 认证域,由服务器返回。

opaque: 透传字符串,客户端应原样返回。

method: 请求的方法。

nonce: 由服务器生成的随机字符串,包含过期时间(默认过期时间300s)和密钥。

nc: 即nonce-count,指请求的次数,用于计数,防止重放攻击。qop被指定时,nc也必须被指定。

cnonce: 客户端发给服务器的随机字符串,qop被指定时,cnonce也必须被指定。

qop: 保护级别,客户端根据此参数指定摘要算法。若取值为 auth,则只进行身份验证;若取值为auth-int,则还需要校验内容完整性,默认的qop为auth。

uri: 请求的uri。

response: 客户端根据算法算出的摘要值,这个算法取决于qop。

algorithm: 摘要算法,目前仅支持MD5。

entity-body: 页面实体,非消息实体,仅在auth-int中支持。

通常服务器端返回的数据包括realm、opaque、nonce、qop等字段,如果客户端需要做出验证回应,就必须按照一定的算法得到一些新的数据并一起返回。在以上各种参数中,对服务器而言,最重要的字段是nonce;对客户端而言,最重要的字段是response。

  • 摘要认证的实现

package com.qf.my.spring.security.demo.config;

import com.qf.my.spring.security.demo.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.web.authentication.www.DigestAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;

/**
* 摘要认证的配置
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class DigestConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   private DigestAuthenticationEntryPoint digestAuthenticationEntryPoint;

   @Autowired
   private MyUserDetailService userDetailService;

   //配置认证入口端点,主要是设置认证参数信息
   @Bean
   public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint(){
       DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();
       point.setKey("security demo");
       point.setRealmName("thor");
       point.setNonceValiditySeconds(500);
       return point;
  }

   public DigestAuthenticationFilter digestAuthenticationFilter(){
       DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
       filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint);
       filter.setUserDetailsService(userDetailService);
       return filter;
  }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
              .antMatchers("/hello").hasAuthority("role")
              .anyRequest().authenticated()
              .and().csrf().disable()
               //当未认证时访问某些资源,则由该认证入口类来处理.
              .exceptionHandling()
              .authenticationEntryPoint(digestAuthenticationEntryPoint)
              .and()
               //添加自定义过滤器到过滤器链中
              .addFilter(digestAuthenticationFilter());

  }
}

 

六、自定义用户名和密码

Spring Security提供了多种方式自定义用户名和密码。

1.使用application.properties

# 配置用户名
spring.security.user.name=qfadmin
# 配置密码
spring.security.user.password=123456

还需要向IOC容器里注入一个PasswordEncoder,用于生成密码的base64编码的字符串,和解析base64编码的字符串为实际密码内容。

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

2.通过创建配置类实现设置

将用户名和密码写在配置类里,虽然配置类中可以自己编写用户名和密码的代码,但因为它是配置类的缘故,不适合将从数据库中获取用户名和密码的业务代码写入到配置类中。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       //用于密码的密文处理
       BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
       //生成密文
       String password = passwordEncoder.encode("123456");
       //设置用户名和密码
       auth.inMemoryAuthentication().withUser("qfAdmin").password(password).roles("admin");
  }
 
   @Bean
   PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }
}

 

3.编写自定义实现类(常用)

  • 设计数据库表

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
 `id` int NOT NULL AUTO_INCREMENT,
 `username` varchar(50) NOT NULL,
 `password` varchar(60) NOT NULL,
 `nickname` varchar(255) DEFAULT NULL,
 `headImgUrl` varchar(255) DEFAULT NULL,
 `phone` varchar(11) DEFAULT NULL,
 `telephone` varchar(30) DEFAULT NULL,
 `email` varchar(50) DEFAULT NULL,
 `birthday` date DEFAULT NULL,
 `sex` tinyint(1) DEFAULT NULL,
 `status` tinyint(1) NOT NULL DEFAULT '1',
 `createTime` datetime NOT NULL,
 `updateTime` datetime NOT NULL,
 PRIMARY KEY (`id`),
 UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

SET FOREIGN_KEY_CHECKS = 1;
  • 使用mybatis-generator生成映射文件

  • 引入Mybatis和连接池的依赖

        <!--        mysql驱动-->
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
       </dependency>

       <!--       druid连接-->
       <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>druid-spring-boot-starter</artifactId>
           <version>1.1.10</version>
       </dependency>

       <!--       mybatis-->
       <dependency>
           <groupId>org.mybatis.spring.boot</groupId>
           <artifactId>mybatis-spring-boot-starter</artifactId>
           <version>1.3.2</version>
       </dependency>
  • 编写application.properties配置文件

# 指明mapper映射文件的位置
mybatis.mapper-locations=classpath:mapper/*.xml
# 配置连接池Druid
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db_security?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=qf123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
  • 启动类上打上注解

@SpringBootApplication
@MapperScan("com.qf.my.ss.demo.mapper")
public class MySsDemoApplication {
   public static void main(String[] args) {
       SpringApplication.run(MySsDemoApplication.class, args);
  }
}
  • 编写UserDetailService实现类

    编写从数据库中获取用户名和密码的业务

package com.qf.my.ss.demo.service;

import com.mysql.cj.util.StringUtils;
import com.qf.my.ss.demo.entity.SysUser;
import com.qf.my.ss.demo.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class MyUserDetailService implements UserDetailsService {

   @Autowired
   private SysUserMapper userMapper;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       //设置角色,角色的概念在之后章节介绍
       List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("user");
       //可以从数据库获取用户名和密码
       if(StringUtils.isNullOrEmpty(username)){
           return null;
      }
       SysUser sysUser = userMapper.selectByUsername(username);
       User user = null;
       if(Objects.nonNull(sysUser)){
           user = new User(username,sysUser.getPassword(),auths);
      }
       return user;
  }
}
  • 编写SecurityConfig配置类,指明对UserDetailsService实现类认证

    @Autowired
   private UserDetailsService userDetailsService;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }

 

七、角色和权限

1.角色和权限的概念

所谓权限,就是用户是否有访问当前页面,或者是执行某个操作的权利。

所谓角色,是对权限的汇总,比如“管理员”角色,可以对数据进行增删改查,增删改查是数据的四个权限,拥有“管理员”角色的用户拥有这四个权限。“普通用户”角色,只具备数据的增和查两种权限,那么拥有“普通用户”角色的用户只拥有这两个权限。

Spring Security提供了四个方法用于角色和权限的访问控制。通过这些方法,对用户是否具有某个或某些权限,进行过滤访问。对用户是否具备某个或某些角色,进行过滤访问。

  • hasAuthority

  • hasAnyAuthority

  • hasRole

  • hasAnyRole

2.hasAuthority方法

判断当前主体是否有指定的权限,有返回true,否则返回false

该方法适用于只拥有一个权限的用户。

  • 在配置类中 设置当前主体具有怎样的权限才能访问。

package com.qf.my.ss.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class PermissionConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //配置没有权限的跳转页面
       http.exceptionHandling().accessDeniedPage("/nopermission.html");
       http.formLogin()
              .loginPage("/login.html") //设置自定义登陆页面
              .loginProcessingUrl("/login") //登陆时访问的路径
              .failureUrl("/error.html")//登陆失败的页面
              .defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
              .and().authorizeRequests()
              .antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
               //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
              .antMatchers("/index.html").hasAuthority("26")
              .anyRequest().authenticated()
              .and().csrf().disable(); //关闭csrf防护
  }

   @Autowired
   private UserDetailsService userDetailsService;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }


   @Bean
   PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }
}
  • 从数据库查询权限的Service

package com.qf.my.ss.demo.service.impl;

import com.mysql.cj.util.StringUtils;
import com.qf.my.ss.demo.entity.SysUser;
import com.qf.my.ss.demo.mapper.SysRolePermissionMapper;
import com.qf.my.ss.demo.mapper.SysRoleUserMapper;
import com.qf.my.ss.demo.mapper.SysUserMapper;
import com.qf.my.ss.demo.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.*;

/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class PermissionServiceImpl implements PermissionService {

   @Autowired
   private SysRoleUserMapper roleUserMapper;

   @Autowired
   private SysRolePermissionMapper rolePermissionMapper;

   @Autowired
   private SysUserMapper userMapper;


   @Override
   public List<Integer> getPermissonsByName(String username) {

       if(StringUtils.isNullOrEmpty(username)){
           return null;
      }
       SysUser sysUser = userMapper.selectByUsername(username);
       List<Integer> permissionIds = new ArrayList<>();
       if(Objects.nonNull(sysUser)){
           Integer id = sysUser.getId();
           List<Integer> roleIds = roleUserMapper.selectByUserId(id);
           if(!CollectionUtils.isEmpty(roleIds)){
               //查询全选
               roleIds.forEach(rid -> {
                   List<Integer> pIds = rolePermissionMapper.selectByRoleId(rid);
                   permissionIds.addAll(pIds);
              });
               //去重
               Set<Integer> pSet = new HashSet<>(permissionIds);
               permissionIds.clear();
               permissionIds.addAll(pSet);

          }
      }
       return permissionIds;
  }
}
  • 在userdetailsService,为返回的User对象设置权限

package com.qf.my.ss.demo.service;

import com.mysql.cj.util.StringUtils;
import com.qf.my.ss.demo.entity.SysUser;
import com.qf.my.ss.demo.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
* @author Thor
* @公众号 Java架构栈
*/
@Service
public class MyUserDetailService implements UserDetailsService {

   @Autowired
   private SysUserMapper userMapper;

   @Autowired
   private PermissionService permissionService;


   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       if(StringUtils.isNullOrEmpty(username)){
           return null;
      }
       //从数据库获得该用户相关的权限
       List<Integer> permissons = permissionService.getPermissonsByName(username);
       //设置权限
       List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(
               permissons.stream().map(String::valueOf).collect(Collectors.joining(",")));
       SysUser sysUser = userMapper.selectByUsername(username);
       User user = null;
       if(Objects.nonNull(sysUser)){
           user = new User(username,sysUser.getPassword(),auths);
      }
       return user;
  }
}

 

 

3.hasAnyAuthority方法

适用于一个主体有多个权限的情况,多个权限用逗号隔开。

package com.qf.my.ss.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class PermissionConfig extends WebSecurityConfigurerAdapter {

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       //配置没有权限的跳转页面
       http.exceptionHandling().accessDeniedPage("/nopermission.html");
       http.formLogin()
              .loginPage("/login.html") //设置自定义登陆页面
              .loginProcessingUrl("/login") //登陆时访问的路径
              .failureUrl("/error.html")//登陆失败的页面
              .defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
              .and().authorizeRequests()
              .antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
               //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
              .antMatchers("/index.html").hasAnyAuthority("26,9")
              .anyRequest().authenticated()
              .and().csrf().disable(); //关闭csrf防护
  }

   @Autowired
   private UserDetailsService userDetailsService;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }


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

 

4.hasRole方法

如果用户具备给定角色就允许访问,否则报403错误。

  • 修改配置类

@Override
   protected void configure(HttpSecurity http) throws Exception {
       //配置没有权限的跳转页面
       http.exceptionHandling().accessDeniedPage("/nopermission.html");
       http.formLogin()
              .loginPage("/login.html") //设置自定义登陆页面
              .loginProcessingUrl("/login") //登陆时访问的路径
              .failureUrl("/error.html")//登陆失败的页面
              .defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
              .and().authorizeRequests()
              .antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
              .antMatchers("/index.html").hasRole("1")
              .anyRequest().authenticated()
              .and().csrf().disable(); //关闭csrf防护
  }
  • 在PermissionServiceImpl添加获得角色的功能

    @Override
   public List<Integer> getRoleByName(SysUser sysUser) {
       return roleUserMapper.selectByUserId(sysUser.getId());
  }

 

  • 修改UserDetailsService

//权限设置
@Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       //根据用户输入的用户名去数据库查询具体的用户对象
       if(StringUtils.isNullOrEmpty(username)){
           return null;
      }
       //数据库查询
       SysUser sysUser = userMapper.selectByUsername(username);
       User user = null;
       if(Objects.nonNull(sysUser)){
           //从数据库获得该用户相关的权限
      List<Integer> permissons = permissionService.getPermissonsByName(username);
      String perString = permissons.stream().map(String::valueOf).collect(Collectors.joining(","));

        //从数据库获得该用户的角色
      SysUser sysUser = userMapper.selectByUsername(username);
      List<Integer> roles = permissionService.getRoleByName(sysUser);
      String roleString = roles.stream().map(num -> "ROLE_" + num).collect(Collectors.joining(","));

      //设置权限
      List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList(perString+","+roleString);

           user = new User(username,sysUser.getPassword(),auths);
      }
       return user;

  }

其中角色student需要在设置时加上“ROLE”前缀,因为通过源码hasRole方法给自定义的角色名前加上了“ROLE”前缀

private static String hasRole(String role) {
       Assert.notNull(role, "role cannot be null");
       Assert.isTrue(!role.startsWith("ROLE_"), () -> {
           return "role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'";
      });
       return "hasRole('ROLE_" + role + "')";
  }

 

5.hasAnyRole方法

设置多个角色,多个角色之间使用逗号隔开,只要用户具有某一个角色,就能访问。

@Override
   protected void configure(HttpSecurity http) throws Exception {
       //配置没有权限的跳转页面
       http.exceptionHandling().accessDeniedPage("/nopermission.html");
       http.formLogin()
              .loginPage("/login.html") //设置自定义登陆页面
              .loginProcessingUrl("/login") //登陆时访问的路径
              .failureUrl("/error.html")//登陆失败的页面
              .defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
              .and().authorizeRequests()
              .antMatchers("/","/login").permitAll() //设置可以直接访问的路径,取消拦截
              .antMatchers("/index.html").hasAnyRole("1","2")
              .anyRequest().authenticated()
              .and().csrf().disable(); //关闭csrf防护
  }

 

八、自动登陆

1. 准备数据库表

创建persistent_logins表,用于持久化自动登陆的信息。

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)

2.实现自动登陆

  • 修改SecurityConfig配置类

package com.qf.my.ss.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
* @author Thor
* @公众号 Java架构栈
*/
@EnableWebSecurity
public class PermissionConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   private DataSource dataSource;

   @Override
   protected void configure(HttpSecurity http) throws Exception {

       //配置数据源
       JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
       tokenRepository.setDataSource(dataSource);
       

       //配置没有权限的跳转页面
       http.exceptionHandling().accessDeniedPage("/nopermission.html");
       http.formLogin()
              .loginPage("/login.html") //设置自定义登陆页面
              .loginProcessingUrl("/login") //登陆时访问的路径
              .failureUrl("/error.html")//登陆失败的页面
              .defaultSuccessUrl("/index.html").permitAll() //登陆成功后跳转的路径
              .and().authorizeRequests()
              .antMatchers("/","/login").permitAll()
              .antMatchers("/index.html").hasRole("1")
              .anyRequest().authenticated()
               //开启记住我功能
              .and().rememberMe().userDetailsService(userDetailsService)
               //持久化令牌方案
              .tokenRepository(tokenRepository)
               //设置令牌有效期,为7天有效期
              .tokenValiditySeconds(60*60*24*7)
              .and().csrf().disable(); //关闭csrf防护
  }

   @Autowired
   private UserDetailsService userDetailsService;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
  }


   @Bean
   PasswordEncoder passwordEncoder(){
       return new BCryptPasswordEncoder();
  }
}
  • 前端页面添加自动登陆表单项

                <div class="checkbox">
                   <label>
                       <input type="checkbox" name="remember-me"> 记住我
                   </label>
               </div>

3.自动登陆底层实现逻辑

  • 首先从前端传来的 cookie 中解析出 series 和 token;

  • 根据 series 从数据库中查询出一个 PersistentRememberMeToken 实例;

  • 如果查出来的 token 和前端传来的 token 不相同,说明账号可能被人盗用(别人用你的令牌登录之后,token 会变)。此时根据用户名移除相关的 token,相当于必须要重新输入用户名密码登录才能获取新的自动登录权限。

  • 接下来校验 token 是否过期;

  • 构造新的 PersistentRememberMeToken 对象,并且更新数据库中的 token(这就是我们文章开头说的,新的会话都会对应一个新的 token);

  • 将新的令牌重新添加到 cookie 中返回;

  • 根据用户名查询用户信息,再走一波登录流程。

 

九、用户注销

1.在配置类添加注销的配置

 @Override
   protected void configure(HttpSecurity http) throws Exception {
       //注销的配置
       http.logout().logoutUrl("/logout") //注销时访问的路径
              .logoutSuccessUrl("/logoutSuccess").permitAll(); //注销成功后访问的路径

       //配置没有权限的跳转页面
       http.exceptionHandling().accessDeniedPage("/error.html");
       http.formLogin()
              .loginPage("/login.html") //设置自定义登陆页面
              .loginProcessingUrl("/usr/login") //登陆时访问的路径
//               .defaultSuccessUrl("/index").permitAll() //登陆成功后跳转的路径
              .defaultSuccessUrl("/success.html").permitAll() //登陆成功后跳转的路径
              .and().authorizeRequests()
                  .antMatchers("/","/add","/user/login").permitAll() //设置可以直接访问的路径,取消拦截
                   //1.hasAuthority方法:当前登陆用户,只有具有admin权限才可以访问这个路径
                   //.antMatchers("/index").hasAuthority("admin")
                   //2.hasAnyAuthority方法:当前登陆用户,具有admin或manager权限可以访问这个路径
                   //.antMatchers("/index").hasAnyAuthority("admin,manager")
                   //3.hasRole方法:当前主体具有指定角色,则允许访问
                   //.antMatchers("/index").hasRole("student")
                   //4.hasAnyRole方法:当前主体只要具备其中某一个角色就能访问
                  .antMatchers("/index").hasAnyRole("student1,teacher")
              .anyRequest().authenticated()
              .and().csrf().disable(); //关闭csrf防护
  }

 

 

2.设置注销链接

添加success.html页面作为登陆成功后的跳转页面

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
  登陆成功 <a href="/logout">退出</a>
</body>
</html>

登陆后访问退出按钮,实现注销功能。

十、JWT(Json Web Token)

1.基于Token的认证方式

使用基于Token的身份验证方法,在服务端不需要存储用户的登陆信息。流程如下:

  • 客户端使用用户名和密码请求登陆。

  • 服务端收到请求,去验证用户名和密码。

  • 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端。

  • 客户端收到Token以后可以把它存储在Cookie本地。

  • 客户端每次向服务端请求资源时需要携带Cookie中该Token。

  • 服务端收到请求后,验证客户端携带的Token,如果验证成功则返回数据。

jwt认证

2.什么是JWT

JSON Web Token (JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对进行签名,防止被篡改。

JWT官网: https://jwt.io

JWT令牌的优点:

  • JWT基于json,非常方便解析。

  • 可以在令牌中自定义丰富的内容,易扩展。

  • 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

  • 资源服务使用JWT可不依赖认证服务即完成授权。

JWT令牌的缺点:

  • JWT令牌较长,占存储空间比较大。

 

3.JWT组成

一个JWT实际上就一个字符串,它由三部分组成,头部、负载与签名。

 

1)头部(Header)

头部用于描述关于该JWT的最基本信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256 或 RSA)等。这也可以被表示成一个JSON对象。

{
 "alg":"HS256",
 "typ":"JWT"
}
  • alg:签名算法

  • typ:类型

我们对头部的json字符串进行BASE64编码,编码后的字符串如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Base64是一种基于64个可打印字符串来表示二进制数据的表示方式。JDK提供了非常方便的Base64Encoder和Base64Decoder,用它们可以非常方便的完成基于Base64的编码和解码。

 

2)负载(Payload)

负载,是存放有效信息的地方,比如用户的基本信息可以存在该部分中。负载包含三个部分:

  • 标准中注册的声明(建议但不强制使用)

    • iss:jwt签发者

    • sub:jwt所面向的用户

    • aud:接收jwt的一方

    • exp:jwt的过期时间,过期时间必须大于签发时间

    • nbf:定义在什么时间之前,该jwt都是不可用的

    • iat:jwt的签发时间

    • jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

  • 公共的声明

公共的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。

  • 私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

私有声明也就是自定义claim,用于存放自定义键值对。

{
 "sub": "1234567890",
 "name": "John Doe",
 "iat": 1516239022
}

其中sub是标准的声明,name是自定义的私有声明,编码后如下:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

 

3)签证、签名(Signature)

jwt的第三部分是一个签证信息,由三部分组成:

  • Header(Base64编码后)

  • Payload(Base64编码后)

  • Secret(盐,必须保密)

这个部分需要Base64加密后的header和base4加密后的payload使用.连接组成的字符串,然后通过header重声明的加密方式进行加盐Secret组合加密,然后就构成了JWT的第三部分——使用“qfjava”作为盐:

eZqdTo1mRMB-o7co1oAiTvNvumfCkt-1H-CdfNm78Cw

从官方工具中可以看到,三个部分组合出的完整字符串:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.eZqdTo1mRMB-o7co1oAiTvNvumfCkt-1H-CdfNm78Cw

 

 

注意:secret是保存在服务器端的,jwt在签发生成也是在服务器端的,secret就是用来进行jwt的签发和验证,所以,它就是服务器端的私钥,在任何场景都不应该泄漏。一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt了。

4.使用JJWT

JJWT是一个提供端到端的JWT创建和验证的开源Java库。也就是说使用JJWT能快速完成JWT的功能开发。

  • 引入依赖

创建Springboot工程并引入jjwt依赖,pom.xml如下:

        <!--jjwt-->
       <dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt</artifactId>
           <version>RELEASE</version>
       </dependency>
  • 创建Token

    @Test
   public void testCrtToken(){

       //创建JWT对象
       JwtBuilder builder = Jwts.builder().setId("1001")//设置负载内容
              .setSubject("小明")
              .setIssuedAt(new Date())//设置签发时间
              .signWith(SignatureAlgorithm.HS256, "qfjava");//设置签名秘钥
       //构建token
       String token = builder.compact();
       System.out.println(token);

  }

 

JWT将用户信息转换成Token字符串,生成结果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAxIiwic3ViIjoi5bCP5piOIiwiaWF0IjoxNjE1MzY2MDEyfQ.2LNcw1v64TNQ96eCpWKvtAccBUA-cEVMDyJNMef-zu0

 

  • 解析Token

通过JWT解析Token,获取Token中存放的用户信息,即生成Claims对象。

    @Test
   public void testParseToken(){
       String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAxIiwic3ViIjoi5bCP5piOIiwiaWF0IjoxNjE1MzY2MDEyfQ.2LNcw1v64TNQ96eCpWKvtAccBUA-cEVMDyJNMef-zu0";
       //解析Token,生成Claims对象,Token中存放的用户信息解析到了claims对象中
       Claims claims = Jwts.parser().setSigningKey("qfjava").parseClaimsJws(token).getBody();
       System.out.println("id:" + claims.getId());
       System.out.println("subject:" + claims.getSubject());
       System.out.println("IssuedAt:" + claims.getIssuedAt());
  }

解析结果如下:

id:1001
subject:小明
IssuedAt:Wed Mar 10 16:46:52 CST 2021

 

  • Token过期检验

在有效期内Token可以正常读取,超过有效期则Token失效

    @Test
   public void testExpToken(){
       long now = System.currentTimeMillis();  //当前时间
       long exp = now + 1000 * 60; //过期时间为1分钟
       JwtBuilder builder = Jwts.builder().setId("1001")
              .setSubject("小明")
              .setIssuedAt(new Date())
              .signWith(SignatureAlgorithm.HS256, "qfjava")
              .setExpiration(new Date(exp));//设置超时
  }

 

  • 自定义claims

除了使用官方api设置属性值,也可以添加自定义键值对。

    @Test
   public void testCustomClaims(){
       long now = System.currentTimeMillis();  //当前时间
       long exp = now + 1000 * 60; //过期时间为1分钟
       JwtBuilder builder = Jwts.builder().setId("1001")
              .setSubject("小明")
              .setIssuedAt(new Date())
              .signWith(SignatureAlgorithm.HS256, "qfjava")
              .setExpiration(new Date(exp))
              .claim("role", "admin");//设置自定义键值对
  }

使用下面语句获取属性值:

claims.get("role")

 

十一、微服务项目-使用Security+JWT实现权限管理

1.前后端分离的权限管理

Spring Security + jwt 前后端分离的权限系统的时序图

2.引入依赖

        <!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

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

<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--druid连接-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>

<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>

<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>RELEASE</version>
</dependency>

<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

3.登陆过滤器的实现

package com.qf.my.security.admin.demo.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.qf.my.security.admin.demo.common.ResponseUtil;
import com.qf.my.security.admin.demo.common.ResultModel;
import com.qf.my.security.admin.demo.entity.SecurityUser;
import com.qf.my.security.admin.demo.entity.User;
import com.qf.my.security.admin.demo.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;

/**
* @author Thor
* @公众号 Java架构栈
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;

public TokenLoginFilter(AuthenticationManager authenticationManager,TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.authenticationManager = authenticationManager;
//不是只允许post请求,经过这个filter
this.setPostOnly(false);
//设置登陆的路径和请求方式
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login","POST"));
}

/**
* 执行认证的方法
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//获取表单提供的数据
ObjectMapper objectMapper = new ObjectMapper();
try {
User user = objectMapper.readValue(request.getInputStream(), User.class);
//校验==认证的过程
Authentication authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()
, new ArrayList<>())
);
return authenticate;

} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("认证失败");
}

}

/**
* 认证成功以后调用的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

//得到用户名
SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
String username = securityUser.getUsername();
//生成token
String token = tokenManager.crtToken(username);
//存入到redis username: 权限
redisTemplate.opsForValue().set(username,securityUser.getPermissionValueList());
//返回token
ResponseUtil.out(response, ResultModel.success(token));

}

/**
* 认证失败调用的方法
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response,ResultModel.error(401,failed.getMessage()));
}
}

4.权限过滤器的实现

package com.qf.my.security.admin.demo.filter;

import com.mysql.cj.util.StringUtils;
import com.qf.my.security.admin.demo.security.TokenManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.CollectionUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
* @author Thor
* @公众号 Java架构栈
*/
public class TokenAuthFilter extends BasicAuthenticationFilter {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenAuthFilter(AuthenticationManager authenticationManager,
TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

/**
* 权限相关的操作
* @param request
* @param response
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获得token
String token = request.getHeader("token");
if(!StringUtils.isNullOrEmpty(token)){
//使用jwt解析token获得username
String username = tokenManager.getUsernameFromToken(token);
//从redis中获得该用户名对应的权限
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(username);
//将取出的权限存入到权限上下文中,表示当前token对应的用户具备哪些权限
Collection<GrantedAuthority> authorityCollection = new ArrayList<>();
if(!CollectionUtils.isEmpty(permissionValueList)){
for (String permissionValue : permissionValueList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorityCollection.add(authority);
}
}
//生成权限信息对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,token,authorityCollection);
//把权限信息对象存入到权限上下文中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
chain.doFilter(request,response);
}
}

5.注销处理器的实现

package com.qf.my.security.admin.demo.security;

import com.mysql.cj.util.StringUtils;
import com.qf.my.security.admin.demo.common.ResponseUtil;
import com.qf.my.security.admin.demo.common.ResultModel;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author Thor
* @公众号 Java架构栈
*/
public class TokenLogoutHandler implements LogoutHandler {

private TokenManager tokenManager;
private RedisTemplate redisTemplate;

public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}

/**
* 注销时具体要执行的业务
* @param request
* @param response
* @param authentication
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1.从请求头中获得前端携带的token
String token = request.getHeader("token");
if(!StringUtils.isNullOrEmpty(token)){
//2.使用jwt解析token
String username = tokenManager.getUsernameFromToken(token);
//3.删除redis中的数据
redisTemplate.delete(username);
}
ResponseUtil.out(response, ResultModel.success("注销成功"));
}
}

6.用户名密码验证逻辑

package com.qf.my.security.admin.demo.service.impl;

import com.qf.my.security.admin.demo.entity.SecurityUser;
import com.qf.my.security.admin.demo.entity.User;
import com.qf.my.security.admin.demo.service.PermissionService;
import com.qf.my.security.admin.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

/**
* @author Thor
* @公众号 Java架构栈
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService userService;

@Autowired
private PermissionService permissionService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名从数据库查询到该用户的信息
User user = userService.selectByUsername(username);
if(Objects.isNull(user)) {
throw new UsernameNotFoundException("当前用户不存在");
}
//根据用户名从数据库查询到该用户的权限信息
List<String> permissionValues = permissionService.selectPermissionValueByUserId(user.getId());
SecurityUser securityUser = new SecurityUser();
securityUser.setCurrentUserInfo(user);
securityUser.setPermissionValueList(permissionValues);
return securityUser;
}
}
 
posted @ 2023-11-15 20:19  尐鱼儿  阅读(152)  评论(0编辑  收藏  举报