SpringSecurity

SpringSecurity

  • 1、前言
  • 2、Spring Security
    • 2.1、Spring Security简介
      • 2.1.1、安全框架
      • 2.1.2、常用安全框架
      • 2.1.3、关于Spring Security
    • 2.2、Spring Security入门
    • 2.3、UserDetailsService
    • 2.4、PasswordEncoder密码解析器
    • 2.5、自定义登录逻辑
    • 2.6、自定义登录页面
    • 2.7、自定义错误页面
    • 2.8、自定义用户名和密码参数名
    • 2.9、自定义成功登录处理器
    • 2.10、自定义失败登录处理器
    • 2.11、关于授权配置
    • 2.12、内置控制访问方法
    • 2.13、角色权限判断
    • 2.14、自定义403处理方案
    • 2.15、基于表达式的访问控制
      • 2.15.1、access()方法
      • 2.15.2、自定义方法
    • 2.16、基于注解的访问控制
      • 2.16.1、@Secured注解
      • 2.16.2、@PreAuthorize和@PostAuthorize注解
    • 2.17、RememberMe实现
    • 2.18、Thymeleaf中使用SpringSecurity
    • 2.19、退出登录
    • 2.20、CSRF
      • 2.20.1、CSRF概念
      • 2.20.2、Spring Security中的CSRF
  • 3、OAuth2.0
    • 3.1、OAuth简介
    • 3.2、OAuth2.0简介
    • 3.3、OAuth2.0的认证流程
    • 3.4、OAuth2.0认证的优缺点
    • 3.5、OAuth2.0的授权模式
      • 3.5.1、授权码模式(Authorization Code)
      • 3.5.2、简化授权模式(Implicit Grant)
      • 3.5.3、密码模式(Resource Owner PasswordCredentials)
      • 3.5.4、客户端模式(Client Credentials)
  • 4、Spring Security + OAuth2.0
    • 4.1、授权服务器
    • 4.2、Spring Security Oauth2架构
    • 4.3、Spring Security Oauth2授权码模式
      • 4.3.1、环境搭建
      • 4.3.2、测试
    • 4.4、Spring Security Oauth2密码模式
      • 4.4.1、环境搭建
      • 4.4.2、测试
    • 4.5、Redis中存储Token令牌
  • 5、JWT
  • 5.1、常见的认证机制
    • 5.1.1、HTTP Basic Auth
    • 5.1.2、Cookie Auth
    • 5.1.3、OAuth
    • 5.1.4、Token Auth
    • 5.2、JWT简介
      • 5.2.1、JWT的概念
      • 5.2.2、JWT的优缺点
      • 5.2.3、JWT组成
    • 5.3、JJWT使用
  • 6、SpringSecurity + OAuth2.0 + JWT
    • 6.1、整合JWT
    • 6.2、扩展JWT的内容
    • 6.3、解析JWT的内容
    • 6.4、JWT刷新令牌
  • 7、SpringSecurity + OAuth2.0 + JWT整合SSO
    • 7.1、SSO单点登录
    • 7.2、测试
  • 8、总结

1、前言

以前对于安全框架不太熟悉,无论是Shiro还是Spring Security。现在Spring用的比较多,所以想好好学一下Spring Security。此篇博客算是在别人那里学习的基础上写的,目的是了解SpringSecurity、Oath2、JWT、SpringSecurity + Oath2、SpringSecurity + Oauth2 +JWT以及SpringSecurity + Oath2 + JWT整合SSO。

以下图示:
SpringSecurity_第1张图片

2、Spring Security

2.1、Spring Security简介

2.1.1、安全框架

安全框架就是解决系统安全的框架。如果没有安全框架,我们需要手动的处理每个资源的访问控制,这是非常麻烦的。使用了安全框架,我们可以通过配置的方式实现对资源的访问限制。

2.1.2、常用安全框架

  • Apache Shiro
    一个功能强大且易于使用的Java安全框架,提供了认证、授权、加密、会话管理。
  • Spring Security
    Spring家族的一员,是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring的IOC(控制反转)、DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,为了减少企业系统安全控制编写大量重复代码的工作。

2.1.3、关于Spring Security

Spring Security是一个高度自定义的安全框架。利用Spring IOC、DI和AOP的功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用Spring Security的原因很多,但大部分都是发现了Java EE的Servlet规范或EJB规范中的安全功能缺乏典型的企业级应用场景,同时认识到他们在WAR或EAR级别无法移植。因此如果更换服务器环境,还有大量工作去重写配置应用程序。使用Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。应用程序的两个主要区域是认证和授权(访问控制)。这两点也是Spring Security重要的核心功能。认证是建立一个他声明的主体的过程,一个主体一般指用户,设备或者一些可以在你的应用程序中执行动作的其他系统,简单来说就是系统认为用户是否能登录。授权指确定一个主体是否允许在你的应用程序中执行一个动作的过程,简单来说就是系统判断用户是否有权限去执行某些操作。

2.2、Spring Security入门

(1)创建工程

创建一个springboot工程,springsecurity-demo。

(2)pom依赖

pom依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
	https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.6.RELEASE</version>
		<relativePath />
	</parent>

	<groupId>com.ycz</groupId>
	<artifactId>spring-security-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springsecurity-demo</name>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
	    <!-- spring security依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		
		<!-- web模块 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</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>
		
		<!-- security测试包 -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

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

</project>

(3)yml配置

application.yml中只有一个端口配置。

server:
  port: 8888

(4)启动类

@SpringBootApplication
public class SpringsecurityDemoApplication {
     

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

}

(5)登录页面和主页面

登录的简单页面login.html如下:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>登录界面</title>
</head>
<body>
	<form action="/login" method="post">
		用户名:<input type="text" name="username" /><br>
		密码:<input type="password" name="password"/><br>
		<input type="submit" value="登录">
	</form>
</body>
</html>

主页面main.html如下:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>主页面</title>
</head>
<body bgcolor="pink">
	<h2 align="center">欢迎来到主页面!</h2>
</body>
</html>

(6)控制器

创建controller包,控制器放在controller包,内容如下:

@Controller
public class LoginController {
     
    
    /*
     * 登录
     */
    @RequestMapping("/login")
    public String login() {
     
        return "redirect:main.html";
    }

}

启动项目,访问localhost:8888/login.html
SpringSecurity_第2张图片
进入了SpringSecurity的内置页面,需要验证,来到控制台:
在这里插入图片描述
这个是验证密码,用户名是user,每次启动都会生成不一样的密码,注意一下。使用用户名和控制台的密码登录:
SpringSecurity_第3张图片
SpringSecurity_第4张图片
验证成功后来到了登录页面。

2.3、UserDetailsService

这个是Spring Security提供的接口,源码如下:
SpringSecurity_第5张图片
用户的登录是访问这个接口的唯一方法loadUserByUsername的,这个方法接收一个参数,就是用户名,如果没有,会抛异常。如果有,返回UserDetails。

UserDetails也是Spring Security提供的一个接口,源码如下:
SpringSecurity_第6张图片
SpringSecurity_第7张图片
SpringSecurity_第8张图片
这个接口有7个抽象方法,值得注意的前3个方法,第1个方法是获取权限的,第2个方法获取密码,第3个方法获取用户名。

UserDetails接口有一个实现类User,部分源码如下:
SpringSecurity_第9张图片
有一个构造方法:
SpringSecurity_第10张图片
这个构造方法有3个参数:用户名、密码、权限。然后里面调用了重载的另一个构造方法:
SpringSecurity_第11张图片
这个重载的构造方法有7个参数,除了用户名、密码、权限外,还有账号是否启用、是否过期、是否锁定等。

这3个东西的位置如下:
在这里插入图片描述

2.4、PasswordEncoder密码解析器

PasswordEncoder是Spring Security提供的一个接口,称它为密码解析器,这个接口主要是处理密码的。源码如下:
SpringSecurity_第12张图片
在这里插入图片描述
接口提供3个方法,第一个方法是对明文密码进行加密的,返回一个密文。第二个方法是匹配明文密码和密文,返回布尔值。第三个方法是对密文进行二次加密,这个方法是默认的。

PasswordEncoder接口有很多实现类,其中最主要的是官方推荐的BCryptPasswordEncoder类,平时使用的最多的就是这个密码解析器。BCryptPasswordEncoder是对bcrypt强散列方法的具体实现,是基于hash算法的单向加密。可以通过strength来控制强度,默认是10。

源码如下:
SpringSecurity_第13张图片
SpringSecurity_第14张图片
encode方法是对明文密码进行加密,原理是使用一个随机生成的salt,用明文密码加上这个salt来一起进行加密,返回密文,由于这个salt每次生成的都不一样,所以即使明文密码一样,最后加密出来的密文是不一样的,这样保证了用户密码的安全。
SpringSecurity_第15张图片
matchs方法是用来匹配明文密码和密文的,最终结果用布尔值返回。

测试加密和匹配:

    // 测试BCryptPasswordEncoder的加密和匹配方法
    @Test
    public void test1() {
     
        String password = "ycz123456";// 密码
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        for (int i = 1; i <= 5; i++) {
     
            // 加密明文密码,返回密文
            String encoder = passwordEncoder.encode(password);
            // 明文和密文进行匹配
            boolean bool = passwordEncoder.matches(password, encoder);
            System.out.println(encoder + ":是否匹配?" + bool);
        }

在这里插入图片描述
可以看到,一样的密码,5次加密后的密文全都不一样,但是全都能匹配上。

2.5、自定义登录逻辑

(1)配置类

当我们自定义登录逻辑时,需要用到UserDetailsService和PasswordEncoder,Spring Security要求自定义登录逻辑时容器内必须要有PasswordEncoder实例,不能new出来,因此需要一个配置类来向容器中注入。

创建config包,包下定义SecurityConfig配置类:

/*
 * Spring Security配置
 */
@Configuration
public class SecurityConfig {
     

    // 向容器中注入PasswordEncoder实例
    @Bean
    public PasswordEncoder getPasswordEncoder() {
     
        return new BCryptPasswordEncoder();
    }

}

(2)UserDetailsService的实现类

需要有一个类来实现UserDetailsService接口,如下:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
     
    
    @Autowired
    private PasswordEncoder passwordEncoder;

    /*
            * 重写loadUserByUsername方法
     */
    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
     
        //实际是根据用户名去数据库查,这里就直接用静态数据了
        if(!username.equals("ycz")) {
     
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //比较密码,匹配成功会返回UserDetails,实际上也会去数据库查
        String password = passwordEncoder.encode("ycz123456");
        User user = new User(username,password,AuthorityUtils.
                commaSeparatedStringToAuthorityList("ycz,admin"));
        return user;
    }

}

启动项目,使用这里的账号和密码来通过Spring Security的验证:
SpringSecurity_第16张图片
SpringSecurity_第17张图片
SpringSecurity_第18张图片
通过了Spring Security的验证,来到了登录页面。

2.6、自定义登录页面

Spring Security提供了登录页面,就是需要验证的那个页面。但是一般在实际项目中会用自己定义好的登录页面,如果想用自己定义好的登录页面,比如这里的login.html,只需要修改配置类即可。

修改后的配置类如下:

/*
 * Spring Security配置
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
     
    
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        //表单提交
        http.formLogin()
        .loginPage("/login.html")//自定义登录页面
        //必须和表单提交的接口路径一致,会去执行自定义登录逻辑
        .loginProcessingUrl("/login")
        //登录成功后跳转到的页面,只接受post请求
        .successForwardUrl("/toMain");
        
        //授权
        http.authorizeRequests()
             .antMatchers("/login.html").permitAll()//放行的路径
             .anyRequest().authenticated();//除了放行路径,其他路径都需要授权
        
        //关闭csrf防护
        http.csrf().disable();
    }

    // 向容器中注入PasswordEncoder实例
    @Bean
    public PasswordEncoder getPasswordEncoder() {
     
        return new BCryptPasswordEncoder();
    }

}

LoginController修改如下:

@Controller
public class LoginController {
     
    
    /*
           * 登录
     */
    @RequestMapping("/toMain")
    public String main() {
     
        return "redirect:main.html";
    }

}

启动项目,访问http://localhost:8888/login.html:
SpringSecurity_第19张图片
输入用户名和密码进行登录:
SpringSecurity_第20张图片
登录成功。来到了主页面。

2.7、自定义错误页面

和自定义登录页面一样,修改配置类:
SpringSecurity_第21张图片
标记的是新添加的内容。

然后LoginController控制器添加:

    /*
           * 错误跳转
     */
    @RequestMapping("/toError")
    public String error() {
     
        return "redirect:error.html";
    }

自定义的错误页面error.html:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>错误页面</title>
</head>
<body>
	<h2 style="color:red" align="center">登录失败,请重新登录!</h2>
	<a href="/login.html">点击跳转</a>
</body>
</html>

启动项目,访问http://localhost:8888/login.html,输入错误的用户名:
SpringSecurity_第22张图片
登录:
SpringSecurity_第23张图片
跳到了自定义的错误页面,点击跳转:
SpringSecurity_第24张图片
回到了登录页面。

2.8、自定义用户名和密码参数名

前面写的登录页面的表单:
SpringSecurity_第25张图片
标记的地方是必须这样写的,也就是固定,提交的地址必须是login,提交方法必须是post,用户名的参数必须是username,密码的参数必须是password。原因是Spring Security定义了一个UsernamePasswordAuthenticationFilter拦截器,拦截器里面已经定死了。

UsernamePasswordAuthenticationFilter拦截器源码如下:
SpringSecurity_第26张图片
上面已经定死了。
SpringSecurity_第27张图片
如果不是post请求提交,直接会抛异常。
SpringSecurity_第28张图片
SpringSecurity_第29张图片
获取用户名参数和密码参数时按照定义的username和password参数来获取,如果表单里面的和这里不一样,是获取不到的。

但是它提供了方法可以修改用户名和密码的参数名:
SpringSecurity_第30张图片
通过这两个方法可以修改参数名称。

修改配置类,如下:
SpringSecurity_第31张图片
标记的地方是添加的。

然后表单的参数名只需和配置类里设置的一样就可以了:
SpringSecurity_第32张图片
启动项目,访问登录页面:
在这里插入图片描述
输入用户名密码登录:
SpringSecurity_第33张图片
登录成功,说明配置起作用了。

2.9、自定义成功登录处理器

前面登录成功后是跳转到新页面的,如下:
SpringSecurity_第34张图片
点进去这个方法:
SpringSecurity_第35张图片
点进去ForwardAuthenticationSuccessHandler:
SpringSecurity_第36张图片
可以看到,底层就是一个forward跳转,我们知道forward跳转是无法跳到应用之外的页面的, 现在进行测试,修改配置类中登录成功后跳转的页面:
SpringSecurity_第37张图片
看能否跳到B站主页。启动项目,访问登录页面:
SpringSecurity_第38张图片
输入用户名、密码登录:
SpringSecurity_第39张图片
无法跳转,因为底层的源码是forward跳转,无法跳到应用之外的页面。那么怎么样跳到应用之外的页面呢?可以通过重定向来实现。观察源码:
SpringSecurity_第40张图片
发现是通过标记的这个方法来实现成功跳转的,那么我们可以自己实现AuthenticationSuccessHandler接口,自己定义这个方法的逻辑,里面不用跳转,使用重定向。

创建新的包handler,包下创建MyAuthenticationSuccessHandler,内容如下:

/*
 * 自定义登录成功后的处理器
 */
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
     

    private String url;

    // 构造方法
    public MyAuthenticationSuccessHandler(String url) {
     
        this.url = url;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
     
        // 获取用户
        User user = (User) authentication.getPrincipal();
        // 打印用户名
        System.out.println(user.getUsername());
        // 密码,出于安全考虑,Spring Security这里会返回一个Null
        System.out.println(user.getPassword());
        // 权限
        System.out.println(user.getAuthorities());
        // 这里使用跳转
        response.sendRedirect(url);

    }

}

然后配置类里修改如下:
SpringSecurity_第41张图片
启动项目,访问登录页面,输入用户名密码进行登录:
SpringSecurity_第42张图片
跳过来了,再看控制台:
SpringSecurity_第43张图片
也打印出内容了。可以看一下Authentication的源码:
SpringSecurity_第44张图片
这个接口是继承了Principal接口,并且有一个这样的方法:
SpringSecurity_第45张图片
刚才调用的就是这个getPrincipal方法,返回一个Object类型对象,里面包含用户的一些信息。

2.10、自定义失败登录处理器

类似成功登录的处理器,先看源码:
SpringSecurity_第46张图片
SpringSecurity_第47张图片
本质上是调用了ForwardAuthenticationFailureHandler处理器的onAuthenticationFailure方法来实现跳转。我们也可以自己实现AuthenticationFailureHandler接口,自定义失败跳转逻辑。

在handler包下创建MyAuthenticationFailureHandler,内容如下:

/*
 * 自定义失败登录跳转处理器
 */
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
     

    private String url;

    // 构造方法
    public MyAuthenticationFailureHandler(String url) {
     
        this.url = url;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
     
        // 失败登录后使用重定向
        response.sendRedirect(url);
    }

}

然后修改配置类:
SpringSecurity_第48张图片
启动项目,访问登录页面,使用错误的用户名密码进行登录:
SpringSecurity_第49张图片
可以跳转。事实上在实际开发中基本都是前后端分离的,使用的都是这两种处理器的方式来进行跳转。

2.11、关于授权配置

(1)anyRequest

这个是所有请求,Spring Security的要求是这个必须要放在最后面,如下:
SpringSecurity_第50张图片
可以理解为拦截器,放行的路径放在前面,从前往后执行,除了放行路径之外的其他路径都需要进行认证才能访问。

(2)antMatchers

这个是放行的路径,放行的路径不需要进行Spring Security即可访问,比如项目的一些css、js、图片等静态资源全部需要放行,如下:
SpringSecurity_第51张图片
这3个文件夹下的内容都要放行,配置如下:
SpringSecurity_第52张图片
启动项目,访问images目录下的一张图片:
SpringSecurity_第53张图片
成功访问。只要满足ant匹配表达式的路径都会放行,如下:
SpringSecurity_第54张图片
启动项目,访问time.jpg图片:
SpringSecurity_第55张图片
这个被拦截了。

访问t.png图片:
SpringSecurity_第56张图片
可以访问,说明放行的路径起作用了。

(3)regexMatchers

可以使用正则表达式来匹配放行路径,如下:
SpringSecurity_第57张图片
访问:
SpringSecurity_第58张图片
还可以规定请求方式:
SpringSecurity_第59张图片
在控制器里添加:
SpringSecurity_第60张图片
放行路径:
SpringSecurity_第61张图片
这里只放行post请求的test路径,访问:
SpringSecurity_第62张图片
被拦截了,因为/test是get请求的,修改放行路径:
在这里插入图片描述
这里改为GET请求,再访问:
SpringSecurity_第63张图片
请求成功,说明放行了。

(4)mvcMatchers

一般和servletPath一起用,相当于加应用前缀。在application.yml中添加:
SpringSecurity_第64张图片

修改配置类:
在这里插入图片描述
启动项目,访问:
SpringSecurity_第65张图片
加前缀再访问:
SpringSecurity_第66张图片
这种方式其实完全可以用antMatchers替代,如下:
在这里插入图片描述
再访问:
SpringSecurity_第67张图片

2.12、内置控制访问方法

在这里插入图片描述
这里的permitAll方法和authenticated方法,看源码:
SpringSecurity_第68张图片
SpringSecurity_第69张图片
可以看到有6种属性,每种属性都对应一个方法,permitAll是允许所有,denyAll是拒绝所有,anonymous是允许匿名的,authenticated是需要认证,fullyAuthenticated是需要完整的认证,rememberMe是记住,比如7天免登录这种。这6个方法无法单独使用,需要配合antMatchers等方法一起使用。

2.13、角色权限判断

除了内置权限控制。Spring Security中还支持很多其他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。

(1)基于权限判断

判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建User对象时指定的。如下:
SpringSecurity_第70张图片
这里直接指定了两个静态的数据作为权限。

主页面中加一个跳转:
SpringSecurity_第71张图片
main1页面如下:
SpringSecurity_第72张图片
修改yml配置文件:
SpringSecurity_第73张图片
修改Spring Security配置文件:
SpringSecurity_第74张图片

拥有ycz这个权限才能访问main1.html这个页面。

启动,登录:
SpringSecurity_第75张图片
点击链接:
SpringSecurity_第76张图片
可以成功访问main1.html页面。

修改配置文件:
在这里插入图片描述
再启动,点击主页面中的链接:
SpringSecurity_第77张图片
访问不了,因为当前用户并没有yan这个权限。也可以指定多个权限,如下:
SpringSecurity_第78张图片
只要用户拥有其中指定的任意一个权限,就能够访问。

启动,登录后点击主页面的链接:
SpringSecurity_第79张图片
成功访问。

(2)基于角色判断

如果用户具备给定角色就允许访问。否则出现 403。参数取值来源于自定义登录逻辑 UserDetailsService实现类中创建 User 对象时给User赋予的授权。
SpringSecurity_第80张图片
必须以ROLE_开头,这是固定格式,后面接角色名称。

修改配置文件:
SpringSecurity_第81张图片
启动工程,主页面中点击链接:
SpringSecurity_第82张图片
可以访问,如果用户没有该角色,就无法访问,类似权限的控制。

(3)基于IP地址控制

只允许指定的IP地址访问,其他的IP拒绝访问,如下:
SpringSecurity_第83张图片
启动,点击主页面中的链接:
在这里插入图片描述
访问成功。

2.14、自定义403处理方案

使用Spring Security时经常会看见40(无权限),默认情况下显示的效果如下:
SpringSecurity_第84张图片
报403页面是因为权限不足。源码:
SpringSecurity_第85张图片
可以自定义类实现这个接口,并且修改规则。

在handler包中新建MyAccessDeniedHandler类,如下:

/*
 * 自定义403处理
 */
 @Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
     

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException accessDeniedException) throws IOException, ServletException {
     
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        // 设置响应头
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        // 向页面中写内容
        printWriter.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
        printWriter.flush();
        printWriter.close();
    }

}

修改配置类:
SpringSecurity_第86张图片
标记的是新添加的。启动工程,点击主页面中的链接:
SpringSecurity_第87张图片
是自定义的json串,说明设置起了作用。

2.15、基于表达式的访问控制

2.15.1、access()方法

先看一下之前的基于角色、权限、IP地址以及内置的访问控制的底层:
SpringSecurity_第88张图片
SpringSecurity_第89张图片
SpringSecurity_第90张图片
SpringSecurity_第91张图片
SpringSecurity_第92张图片
SpringSecurity_第93张图片
可以看到,权限判断实际上底层实现都是调用access(表达式)。

以下为常见的内置表达式:
SpringSecurity_第94张图片
SpringSecurity_第95张图片
之前的权限控制其实使用access表达式也可以达到相同的控制效果,比如下面:
SpringSecurity_第96张图片
可以修改成下面这样:
SpringSecurity_第97张图片
启动工程测试:
SpringSecurity_第98张图片
可以访问。

2.15.2、自定义方法

在实际项目中很有可能出现需要自己自定义逻辑的情况。比如判断登录用户是否具有访问当前URL权限。

新建接口:

public interface MyService {
     
    
    //判断是否有权限
    boolean hasPermission(HttpServletRequest request,Authentication authentication);

}

接口的实现类:

@Component
public class MyServiceImpl implements MyService {
     

    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
     
        // 获取当前主体
        Object obj = authentication.getPrincipal();
        // 如果属于UserDetails
        if (obj instanceof UserDetails) {
     
            // 下转型
            UserDetails userDetails = (UserDetails) obj;
            // 获取主体的所有权限
            Collection<? extends GrantedAuthority> authorities = userDetails
                    .getAuthorities();
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(
                                      request.getRequestURI());
            boolean res = authorities.contains(simpleGrantedAuthority);
            return res;
        }
        return false;
    }

}

修改配置类:
SpringSecurity_第99张图片
启动项目,登录:
SpringSecurity_第100张图片
没有权限,原因是登录成功后的url应该是/main.html,但是在这里并没有:
SpringSecurity_第101张图片
需要添加进去:
SpringSecurity_第102张图片
启动,再登录:
SpringSecurity_第103张图片
可以登录进去。

2.16、基于注解的访问控制

在Spring Security中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity进行开启后使用。如果设置的条件允许,程序正常执行,如果不允许会报500错误。

这些注解可以写到Service接口或方法上,也可以写Controller或Controller的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。

2.16.1、@Secured注解

@Secured是专门用于判断是否具有角色的,能写在方法或类上,参数要以 ROLE_开头。源码如下:
SpringSecurity_第104张图片
(1)开启注解

在启动类或者配置类等能够扫描的类上添加,这里直接在配置类上添加:
SpringSecurity_第105张图片
说明一下,这个注解里面的所有值都是默认为false的,要用的话必须手动改为true。源码如下:
SpringSecurity_第106张图片

(2)在Controller的方法上添加注解

在需要的方法上添加@Secured注解:
SpringSecurity_第107张图片
(3)配置类

配置类需要修改一下,将之前通过配置判断权限都取消,要不然可能出问题:
SpringSecurity_第108张图片
启动项目,测试登录:
SpringSecurity_第109张图片
登录成功。然后将注解中的角色改成错的,再登录:
SpringSecurity_第110张图片
SpringSecurity_第111张图片
控制台报错:
SpringSecurity_第112张图片
因为角色不对,方法是受保护的,不允许访问。可以看出,只有角色正确才能够访问对应的方法。@Secured注解和hasRole这个方法的作用是一样的,通过角色判断。

2.16.2、@PreAuthorize和@PostAuthorize注解

@PreAuthorize和@PostAuthorize都是方法或类级别注解。源码如下:
SpringSecurity_第113张图片
SpringSecurity_第114张图片
这两个注解的区别如下:

  • @PreAuthorize:表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数取值相同,都是权限表达式。
  • @PostAuthorize:表示方法或类执行结束后判断权限,此注解很少被使用到。

实际开发中基本上用的都是@PreAuthorize注解,在执行之前进行判断,判断是否具有操作方法的权限,如果是之后再判断,那就没什么意义了,所以@PostAuthorize注解用的不多。

(1)开启注解

和上面的类似,这里也是在配置类上开启:
在这里插入图片描述
(2)在Controller的方法上添加注解

在控制器方法上添加@PreAuthorize,参数可以是任何 access()支持的表达式。
SpringSecurity_第115张图片

这里是对角色判断,并且加了前缀ROLE_的,先测试错误的角色:
SpringSecurity_第116张图片
再测试正确的角色:
SpringSecurity_第117张图片
SpringSecurity_第118张图片
可以访问。然后测试不加前缀:
SpringSecurity_第119张图片
SpringSecurity_第120张图片
还是以访问,说明一个问题:如果是在@PreAuthorize注解中的话,可以加ROLE_前缀,也以不加,但是在配置中一定不能加前缀。

再测试通过权限访问:
SpringSecurity_第121张图片
SpringSecurity_第122张图片
可以访问,那么发现@PreAuthorize注解对角色和权限控制都行,事实上,只要是access()方法里面的,这个注解都支持。

2.17、RememberMe实现

Spring Security中Remember Me为“记住我”功能,用户只需要在登录时添加remember-me复选框,取值为true。Spring Security会自动把用户信息存储到数据源中,以后就可以不登录进行访问。

(1)前端

在login.html中添记住我复选框:
SpringSecurity_第123张图片
(2)pom依赖

会用到数据库,pom依赖中添加MySQL驱动包和spring mybatis的整合包:

		<!-- mysql驱动包 -->
		<dependency>
		    <groupId>mysql</groupId>
		    <artifactId>mysql-connector-java</artifactId>
		    <version>8.0.11</version>
		</dependency>
		
		<!-- mybatis和spring的整合包 -->
		<dependency>
		    <groupId>org.mybatis.spring.boot</groupId>
		    <artifactId>mybatis-spring-boot-starter</artifactId>
		    <version>2.1.1</version>
		</dependency>

(3)yml中添加数据源

application中添加数据源信息:

spring:
  datasource:
      url: jdbc:mysql://rm-m5e130nm7h37n6v982o.mysql.rds.aliyuncs.com:3306/security?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
      username: xxxxxxx
      password: xxxxxxx
      driverClassName: com.mysql.cj.jdbc.Driver

需要在Mysql中创建一个新的数据库security。

(4)配置类

在config包下新建一个RememberMeConfig,如下:

/*
 * 记住我功能配置类
 */
@Configuration
public class RememberMeConfig {
     

    // 注入数据源
    @Autowired
    private DataSource dataSource;

    // 注入Bean
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository() {
     
        JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl = new JdbcTokenRepositoryImpl();
        // 设置数据源
        jdbcTokenRepositoryImpl.setDataSource(dataSource);
        // 自动建表,第一次运行设为true,以后都设为false
        jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
        return jdbcTokenRepositoryImpl;
    }

}

(5)修改SecurityConfig配置

在SecurityConfig配置类中添加:

    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;
    
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;

        //记住我功能
        http.rememberMe()
             //参数名,和表单中的一样
             .rememberMeParameter("rememberMe")
             //持久层对象
             .tokenRepository(persistentTokenRepository)
             //登录逻辑设置
             .userDetailsService(userDetailsServiceImpl)
             //失效时间,默认为两周,这里设为60秒
             .tokenValiditySeconds(60);

(6)测试

启动工程,查看数据库:
在这里插入图片描述
SpringSecurity_第124张图片
SpringSecurity_第125张图片

自动建了一张表,表中无数据。

访问登录页面进行登录,勾选复选框:
SpringSecurity_第126张图片
SpringSecurity_第127张图片
登录成功,查看数据库中的表:
SpringSecurity_第128张图片
表中自动添加了1条数据。

然后关闭浏览器,再打开,直接访问http://localhost:8888/main.html
SpringSecurity_第129张图片
不用登录,可以访问。等1分钟后,再关闭浏览器,再打开,直接访问主页面:
SpringSecurity_第130张图片
失效了,再次登录,勾选复选框:
SpringSecurity_第131张图片
登录成功,再查看数据表:
SpringSecurity_第132张图片
新增了一条数据,也就是说,用户每次登录,只要勾选了记住我复选框,登录成功后都会往表中写入一条数据,表的时间字段是用来和用户再次登录的时间戳进行对比的,如果时间差小于1分钟,就是免登录,如果时间差大于1分钟,记住我功能就会失效,需要用户重新进行登录。

2.18、Thymeleaf中使用SpringSecurity

Spring Security可以在一些视图技术中进行控制显示效果。例如: JSP或Thymeleaf 。在非前后端分离且使用Spring Boot的项目中多使用Thymeleaf作为视图展示技术。Thymeleaf对Spring Security的支持都放在thymeleaf-extras-springsecurityX中,目前最新版本为5。

(1)pom依赖

在pom文件中添加:

		<!-- thymeleaf springsecurity5整合依赖 -->
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity5</artifactId>
		</dependency>

		<!-- thymeleaf依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

(2)前端页面

在templates目录下创建show.html,内容如下:

<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>展示页面</title>
</head>
<body bgcolor="orange">
 登录账号:<span sec:authentication="name"></span><br/>
 登录账号:<span sec:authentication="principal.username"></span><br/>
 凭证:<span sec:authentication="credentials"></span><br/>
 权限和角色:<span sec:authentication="authorities"></span><br/>
 客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
 sessionId:<span sec:authentication="details.sessionId"></span><br/>
</body>
</html>

在html页面中引入thymeleaf命名空间和security命名空间:
SpringSecurity_第133张图片
可以在html页面中通过sec:authentication=""获取UsernamePasswordAuthenticationToken中所有getXXX的内容,包含父类中的getXXX的内容。

根据源码得出下面属性:

  • name :登录账号名称。
  • principal :登录主体,在自定义登录逻辑中是UserDetails。
  • credentials :凭证。
  • authorities :权限和角色。
  • details :实际上是WebAuthenticationDetails的实例。可以获取remoteAddress(客户端ip)和sessionId(当前 sessionId)。

(3)controller

在LoginController中添加如下方法:

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

启动工程,登录,然后访问:http://localhost:8888/show。
SpringSecurity_第134张图片
(4)权限判断

先给用户主体添加两个新的权限:
在这里插入图片描述

然后在show.html中添加以下内容:
SpringSecurity_第135张图片
show.html完整内容如下:

<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>展示页面</title>
</head>
<body bgcolor="orange">
<h4>
 登录账号:<span sec:authentication="name"></span><br/>
 登录账号:<span sec:authentication="principal.username"></span><br/>
 凭证:<span sec:authentication="credentials"></span><br/>
 权限和角色:<span sec:authentication="authorities"></span><br/>
 客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
 sessionId:<span sec:authentication="details.sessionId"></span><br/>
通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增</button>
<button sec:authorize="hasAuthority('/delete')">删除</button>
<button sec:authorize="hasAuthority('/update')">修改</button>
<button sec:authorize="hasAuthority('/select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('ycz')">新增</button>
<button sec:authorize="hasRole('ycz')">删除</button>
<button sec:authorize="hasRole('ycz')">修改</button>
<button sec:authorize="hasRole('ycz')">查看</button>
</h4>
</body>
</html>

启动工程,登录,再访问http://localhost:8888/show。
SpringSecurity_第136张图片
按照权限判断的话用户只拥有添加和删除权限,上面确实只显示了这两个按钮。
按照角色判断的话,应该四个按钮都显示,结果也是这样。

2.19、退出登录

用户只需要向Spring Security项目中发送/logout 退出请求即可。

在main.html中添加退出链接:
SpringSecurity_第137张图片
修改配置类:
SpringSecurity_第138张图片
启动,登录到主页面:
SpringSecurity_第139张图片
点击退出登录的链接:
SpringSecurity_第140张图片
成功退出,来到了登录页面。

源码分析:
SpringSecurity_第141张图片
SpringSecurity_第142张图片
SpringSecurity_第143张图片
SpringSecurity_第144张图片
SpringSecurity_第145张图片

按照源码的内容我们可以得出结论:退出的时候,session失效,并且将认证置为空,清空了上下文。

2.20、CSRF

前面其实已经配置过了:
在这里插入图片描述
如果没有这行代码会导致用户无法被认证。这行代码的含义是:关闭csrf防护。

2.20.1、CSRF概念

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。

跨域:只要网络协议,ip地址,端口中任何一个不相同就是跨域请求。

客户端与服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。

2.20.2、Spring Security中的CSRF

从Spring Security4开始CSRF防护默认开启,默认会拦截请求。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。下面将进行测试。

(1)前端

在templates目录下创建login.html,注意和static下的区分,内容如下:

<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>登录界面</title>
</head>
<body>
	<form action="/login" method="post">
		用户名:<input type="text" name="loginName" /><br>
		密码:<input type="password" name="loginpwd"/><br>
		<input type="submit" value="登录">
	</form>
</body>
</html>

(2)controller

在LoginController中添加:

    @RequestMapping("/showLogin")
    public String showLogin() {
     
        return "login";
    }

(3)配置类修改

修改SecurityConfig配置类如下:
SpringSecurity_第146张图片

标记的地方需要修改成showLogin。

然后csrf需要开启:
在这里插入图片描述
这行代码注释掉。然后启动工程测试:
SpringSecurity_第147张图片
以这个url访问登录页面,注意,这个登录页面是templates下的模板,不是原来的static目录下的login.html。输入用户名密码,登录:
SpringSecurity_第148张图片
因为开启了CSRF,所以是认证不成功的。

修改登录模板文件:
SpringSecurity_第149张图片
添加一个隐藏的文本框即可。

再启动工程测试:
SpringSecurity_第150张图片
登录成功。打开浏览器的调试工具:
SpringSecurity_第151张图片
可以看到表单提交的时候提交了csrf的值,说明在页面中获取到了。csrf在实际开发中非常重要,对于用户的账号安全起着至关重要的作用,可以防止用户的sessionId被第三方劫持,因此csrf都不会关闭,会一直保持开启状态。

3、OAuth2.0

3.1、OAuth简介

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。oAuth是Open Authorization的简写。它是一种开放的协议,第三方可以使用OAUTH认证服务。

第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。

3.2、OAuth2.0简介

OAuth2.0是OAuth协议的延续版本,但不向前兼容OAuth 1.0(即完全废止了OAuth1.0)。 OAuth 2.0关注客户端开发者的简易性。要么通过组织在资源拥有者和HTTP服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。2012年10月,OAuth 2.0协议正式发布为RFC 6749 。因为1.0版本过于复杂,到目前已经完成废止了,现在2.0版本已得到广泛应用。

以京东网站使用微信认证的过程来说明OAuth2.0认证:

SpringSecurity_第152张图片

  • 用户进入京东网站的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。 SpringSecurity_第153张图片
    点击“微信”出现一个二维码,此时用户扫描二维码,开始给网站授权。
    SpringSecurity_第154张图片
  • 资源拥有者同意给客户端授权
    资源拥有者扫描二维码表示资源拥有者同意给客户端授权(这里的客户端指的是网站应用平台),微信会对资源拥有者的身份进行验证,验证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到网站。
    SpringSecurity_第155张图片
    点击确认,然后会重定向到京东首页。
    SpringSecurity_第156张图片
    已经登录成功了。
  • 客户端获取到授权码,请求认证服务器申请令牌。此过程对于用户来说是不透明的,客户端应用程序请求认证服务器,请求携带授权码。
  • 认证服务器向客户端响应令牌
    认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功。
  • 客户端请求资源服务器的资源
    客户端携带令牌访问资源服务器的资源。网站携带令牌请求访问微信服务器获取用户的基本信息。
  • 资源服务器返回受保护资源
    资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。

资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务,资源服务器通常要请求认证服务器来校验令牌的合法性。

后面这4步用户其实是看不到的,用户只用扫码并在微信同意授权,然后就登录成功,后面的这几步是由应用来完成的。

3.3、OAuth2.0的认证流程

Oauth2.0认证流程如下:
SpringSecurity_第157张图片
过程说明:

  • 客户端请求资源拥有者的授权。
  • 资源拥有者同意授权,并且返回给客户端一个授权凭证。
  • 客户端带着授权凭证去认证服务器进行认证。
  • 认证通过后会颁发给客户端一个访问令牌。
  • 客户端携带这个访问令牌去请求资源服务器上的资源。
  • 验证访问令牌,令牌合法后资源服务器会将客户端请求的资源返给客户端。

认证流程中的各个角色:

客户端

本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器端)、微信客户端等。

资源拥有者

通常为用户,也可以是应用程序,即该资源的拥有者。

授权服务器(也称认证服务器)

用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授权后方可访问。

资源服务器

存储资源的服务器,比如,网站用户管理服务器存储了网站用户信息,网站相册服务器存储了用户的相册信息,微信的资源服务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息。

一些专有名词:

  • 客户凭证(client Credentials) :客户端的clientId和密码用于认证客户。
  • 令牌(tokens) :授权服务器在接收到客户请求后,颁发的访问令牌。
  • 作用域(scopes) :客户请求访问令牌时,由资源拥有者额外指定的细分权限(permission)。

令牌类型:

  • 授权码 :仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌。
  • 访问令牌 :用于代表一个用户或服务直接去访问受保护的资源。
  • 刷新令牌 :用于去授权服务器获取一个刷新访问令牌。
  • BearerToken :不管谁拿到Token都可以访问资源,类似现金。
  • Proof of Possession(PoP) Token :可以校验client是否对Token有明确的拥有权。

3.4、OAuth2.0认证的优缺点

优点:

  • 更安全,客户端不接触用户密码,服务器端更易集中保护。
  • 广泛传播并被持续采用。
  • 短寿命和封装的token。
  • 资源服务器和授权服务器解耦。
  • 集中式授权,简化客户端。
  • HTTP/JSON友好,易于请求和传递token。
  • 考虑多种客户端架构场景。
  • 客户可以具有不同的信任级别。

缺点:

  • 协议框架太宽泛,造成各种实现的兼容性和互操作性差。
  • 不是一个认证协议,本身并不能告诉你任何用户信息。

3.5、OAuth2.0的授权模式

OAuth2.0有4种授权模式:授权码模式、简化授权模式、密码模式、客户端模式。使用的最多的是授权码模式,这种在实际项目中用的最多,也最安全。

3.5.1、授权码模式(Authorization Code)

流程图如下:
SpringSecurity_第158张图片
过程说明:

  • 用户访问页面。
  • 访问页面将请求重定向到认证服务器。
  • 认证服务器向用户展示授权页面,等待用户授权。
  • 用户同意授权,认证服务器生成一个code和带上client_id发送给应用服务器。然后,应用服务器拿到code,并用client_id去后台查询对应的client_secret。
  • 将code、client_id、client_secret传给认证服务器换取access_token和refresh_token。
  • 认证服务器将access_token和refresh_token返给应用服务器。
  • 应用服务器携带访问令牌access_token访问资源服务器的资源。

这种模式是最复杂也是最安全的,用的最多的,重点掌握这种模式。

3.5.2、简化授权模式(Implicit Grant)

流程图如下:

SpringSecurity_第159张图片
过程说明:

  • 客户端访问应用页面,应用带着clientId将请求重定向到认证服务器。
  • 认证服务器需要用户授权,用户同意授权。
  • 认证服务器会向应用页面返回一个重定向的URI和存在Fragment中的Token,这个Token是无法获取到的。
  • 应用页面根据返回的URI重定向到另一个客户端,这个客户端会返回一个脚本Script给应用。
  • 应用页面会根据这个脚本来解析Token获取令牌,将令牌返回给客户端。
  • 客户端根据令牌获取资源服务器的资源。

这种用的比较少。

3.5.3、密码模式(Resource Owner PasswordCredentials)

流程图如下:
SpringSecurity_第160张图片
过程说明:

  • 资源拥有者在客户端输入用户名和密码。
  • 客户端带着用户名和密码去认证服务器认证。
  • 认证通过,认证服务器向客户端返回一个令牌Token。
  • 客户端携带令牌去资源服务器获取资源。

这种用的也很少。

3.5.4、客户端模式(Client Credentials)

流程图如下:
SpringSecurity_第161张图片
说明:

  • 客户端请求认证服务进行认证。
  • 认证通过,认证服务向客户端返回一个令牌Token。
  • 客户端携带令牌访问资源服务的资源。

比如Docker用的就是这种。

刷新令牌(Refresh Token)
SpringSecurity_第162张图片
刷新令牌Refresh Token是用于Access Token访问令牌过期的时候,不用从头开始认证,直接拿着Refresh Token去认证服务,认证服务直接颁发一个新的访问令牌Access Token给客户端,客户端带着新的访问令牌访问资源服务器的资源。

4、Spring Security + OAuth2.0

4.1、授权服务器

SpringSecurity_第163张图片
授权服务器中有4个端点。说明如下:

  • Authorize Endpoint :授权端点,进行授权。
  • Token Endpoint :令牌端点,经过授权拿到对应的Token。
  • lntrospection Endpoint :校验端点,校验Token的合法性。
  • Revocation Endpoint :撤销端点,撤销授权。

4.2、Spring Security Oauth2架构

SpringSecurity_第164张图片
说明如下:

  • 用户访问,此时没有Token。Oauth2RestTemplate会报错,这个报错信息会被
    Oauth2ClientContextFilter捕获并重定向到认证服务器。
  • 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端。
  • 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端。
  • 客户端拿到Token去资源服务器访问资源,一般会通过Oauth2AuthenticationManager调用ResourceServerTokenServices进行校验。校验通过可以获取资源。

4.3、Spring Security Oauth2授权码模式

4.3.1、环境搭建

(1)创建项目
SpringSecurity_第165张图片
包结构如上图。

(2)pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
	https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath />
	</parent>

	<groupId>com.ycz</groupId>
	<artifactId>spring-security-oauth2-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springsecurity-oauth2-demo</name>

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
	</properties>

	<dependencies>

		<!-- spring cloud中的oauth2依赖 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>

		<!-- spring cloud中的security依赖 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-security</artifactId>
		</dependency>

		<!-- web模块 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</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>
	</dependencies>

	<!-- 引入spring cloud -->
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${
     spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

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

</project>

(3)pojo

在pojo包下自定一个实体类User,但是此类必实现UserDetails接口。如下:

/*
 * 自定义User类,需实现UserDetails接口
 */
public class User implements UserDetails {
     

    private String username;

    private String password;

    private List<GrantedAuthority> authorities;

    // 构造方法
    public User(String username, String password, List<GrantedAuthority> authorities) {
     
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

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

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

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

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

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

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

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

}

(4)Spring Security配置类

config包下创建SecurityConfig配置类,如下:

/*
 * Spring Security配置类
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
     
    
    

    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        http.authorizeRequests()
             .antMatchers("/oauth/**","/login/**","/logout/**").permitAll()//放行
             .anyRequest().authenticated()//其他路径拦截
             .and()
             .formLogin().permitAll()//表单提交放行
             .and()
             .csrf().disable();//csrf关闭
    }

    // 注册PasswordEncoder
    @Bean
    public PasswordEncoder getPasswordEncoder() {
     
        return new BCryptPasswordEncoder();
    }

}

(5)自定义登录逻辑

service包下创建UserDetailsServiceImpl类,如下:

/*
 * 自定义登录逻辑
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
     
    
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
     
        // 密码加密
        String password = passwordEncoder.encode("ycz123456");
        //创建User用户,自定义的User
        User user = new User(username,password,AuthorityUtils.
                commaSeparatedStringToAuthorityList("ycz"));
        return user;
    }

}

(6)认证服务配置

在config包下创建认证服务的配置类AuthorizationServerConfig,如下:

/*
 * 授权服务配置
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
     
    
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
             
         clients.inMemory()//内存中
                 .withClient("client")//客户端ID
                 .secret(passwordEncoder.encode("ycz123456"))//秘钥
                 .redirectUris("http://www.baidu.com")//重定向到的地址
                 .scopes("all")//授权范围
                 .authorizedGrantTypes("authorization_code");//授权类型为授权码模式
    }
    
}

(7)资源服务配置

在config包下创建资源服务的配置类

/*
 * 资源服务配置
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
     

    @Override
    public void configure(HttpSecurity http) throws Exception {
     
        http.authorizeRequests()
             .anyRequest().authenticated()
             .and()
             .requestMatchers()
             .antMatchers("/user/**");
    }
    
   
}

(8)controller

在controller包下创建UserController类,如下:

@RestController
@RequestMapping("/user")
public class UserController {
     
    
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
     
        return authentication.getPrincipal();
    }

}

4.3.2、测试

(1)获取授权码

启动项目,访问:
http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all

说明:

  • http://localhost:9999:这是项目端口。
  • /oauth/authorize?response_type=code:获取授权码的固定写法。
  • client_id:这是客户端ID,就是在授权服务中定义的:
    SpringSecurity_第166张图片
  • redirect_uri:重定向的url。
    SpringSecurity_第167张图片
  • scope:授权范围。
    SpringSecurity_第168张图片
    访问后:
    SpringSecurity_第169张图片
    用户名随便写,只要密码正确就可以。
    SpringSecurity_第170张图片
    勾选,然后授权。
    SpringSecurity_第171张图片
    重定向到了百度首页,并且拿到了授权码。

(2)获取令牌

因为要发送post请求,所以使用postman。

url:http://localhost:9999/oauth/token
SpringSecurity_第172张图片
左边的type选择Basic Auth,右边的用户名为客户端ID,密码是定义好的。
SpringSecurity_第173张图片
body里面选择表单,需要携带5个参数。参数说明如下:

  • grant_type :授权类型,填写authorization_code,表示授权码模式。
  • code :授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • client_id:客户端标识。
  • redirect_uri :申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。
  • scope:授权范围。

发送请求,返回如下:
在这里插入图片描述

通行令牌access_token拿到了。如果授权码不对,是拿不到令牌的,如下:
SpringSecurity_第174张图片
只有授权码正确,服务端才会返回通行令牌,注意。

(3)获取资源服务器资源

需要携带通行令牌来获取。还是post请求:

http://localhost:9999/user/getCurrentUser
SpringSecurity_第175张图片

左边选择Bearer Token,右边Token里面填刚才返回的Token。

发送请求,返回结果如下:
SpringSecurity_第176张图片
获取到了用户信息。然后将令牌修改成错误的,再发送:
SpringSecurity_第177张图片
无效令牌,拒绝访问。只有携带正确的令牌才能访问资源。

4.4、Spring Security Oauth2密码模式

4.4.1、环境搭建

直接在授权码模式的基础上进行修改了。

(1)修改SecurityConfig

直接在里面加:

    //注册AuthenticationManager
    @Bean
    public AuthenticationManager getAuthenticationManager() throws Exception {
     
        return super.authenticationManager();
    }

(2)修改AuthorizationServerConfig

直接在里面加:

    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private UserDetailsServiceImpl userDetailsServiceImpl;
    
    //密码模式需要配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) 
            throws Exception {
     
        endpoints.authenticationManager(authenticationManager)
                 .userDetailsService(userDetailsServiceImpl);
    }

然后下面添加密码模式:
SpringSecurity_第178张图片

4.4.2、测试

(1)获取Token令牌

启动项目,直接在postman中发送:
SpringSecurity_第179张图片
这里不变。
SpringSecurity_第180张图片
表单的参数如上,grant_type的值需要改成password,username的值随便,因为逻辑中没有规定用户名,实际上是需要规定的,password的值必须要写对。

发送,返回结果如下:
SpringSecurity_第181张图片
令牌拿到了。

(2)获取资源

现在直接可以携带令牌去访问资源:
SpringSecurity_第182张图片
发送,返回结果如下:
SpringSecurity_第183张图片
获取资源成功。

4.5、Redis中存储Token令牌

将token直接存在内存中,这在生产环境中是不合理的,下面将其改造成存储在Redis中。

(1)pom依赖

在pom中添加如下依赖:

		<!-- redis依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
   
        <!-- commons-pool2对象连接池 -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
		</dependency>

(2)yml配置

在application.yml中添加:

spring:
  redis:
    host: localhost
    port: 6379
    password: 123456

(3)Redis配置类

在config包下创建RedisConfig配置类,如下:

/*
 * Redis配置类
 */
public class RedisConfig {
     
    
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    
    //注册TokenStore
    @Bean
    public TokenStore reidsTokenStore() {
     
        return new RedisTokenStore(redisConnectionFactory);
    }

}

(4)修改授权服务配置

修改AuthorizationServerConfig,如下:
SpringSecurity_第184张图片
标记的地方是添加的。

(5)测试

启动工程,使用密码模式获取令牌:
SpringSecurity_第185张图片
发送,返回结果:
SpringSecurity_第186张图片
查看redis:
SpringSecurity_第187张图片
存的token和和返回的token是一致的。
SpringSecurity_第188张图片
这里存的是客户端ID。
SpringSecurity_第189张图片
这里存的是用户名。

5、JWT

5.1、常见的认证机制

5.1.1、HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth。

5.1.2、Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie expire time使cookie在一定时间内有效。
SpringSecurity_第190张图片

5.1.3、OAuth

OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
SpringSecurity_第191张图片
这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。缺点是过重。

5.1.4、Token Auth

SpringSecurity_第192张图片
流程如下:

  • 客户端使用用户名跟密码请求登录。
  • 服务端收到请求,去验证用户名与密码。
  • 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端。
  • 客户端收到Token以后可以把它存储起来,比如放在Cookie里。
  • 客户端每次向服务端请求资源的时候需要带着服务端签发的Token。
  • 服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据。

这种方式比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方式更加轻量。

Token机制相对于Cookie机制的优点如下:

  • 支持跨域访问
    Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户
    认证信息通过HTTP头传输。
  • 无状态(也称:服务端可扩展行)
    Token机制在服务端不需要存储session信息,因为Token自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息。
  • 更适用CDN
    可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片
    等),而你的服务端只要提供API即可。
  • 去耦
    不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可。
  • 更适用于移动应用
    当你的客户端是一个原生平台(iOS, Android,Windows 10等)时,Cookie
    是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  • CSRF
    因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  • 性能
    一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多。
  • 不需要为登录页面做特殊处理
    如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理。
  • 基于标准化
    你的API可以采用标准化的JSON Web Token (JWT)这个标准已经存在多个后端库
    (.NET,Ruby,Java,Python,PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。

5.2、JWT简介

5.2.1、JWT的概念

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

5.2.2、JWT的优缺点

优点:

  • jwt基于json,非常方便解析。
  • 可以在令牌中自定义丰富的内容,易扩展。
  • 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  • 资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

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

5.2.3、JWT组成

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

(1)头部(Header)

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

  • alg :签名的算法,这里使用的算法是HS256算法。
  • typ :是类型。

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder ,用它们可以非常方便的完成基于 BASE64 的编码和解码。

(2)负载(Payload)

第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分,如下:

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

  • iss:jwt签发者。
  • sub:jwt所面向的用户。
  • aud:接收jwt的一方。
  • exp:jwt的过期时间,这个过期时间必须要大于签发时间。
  • nbf:定义在什么时间之前,该jwt都是不可用的.。
  • iat:jwt的签发时间。
  • jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明

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

私有的声明

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

这个指的就是自定义的claim。比如下面那个举例中的name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。
SpringSecurity_第193张图片
其中sub是标准的声明, name是自定义的声明(公共的或私有的)。然后将其进行base64编码,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbWVzIiwiYWRtaW4iOnRydWV9

注意:声明中不要放一些敏感信息,比如密码。

(3)签证、签名(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:header (base64后的)、payload (base64后的)、secret(盐,一定要保密)。

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:

8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

5.3、JJWT使用

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

以下通过一个demo来快速入门。

(1)创建工程
SpringSecurity_第194张图片
(2)pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
	https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.8.RELEASE</version>
		<relativePath />
	</parent>

	<groupId>com.ycz</groupId>
	<artifactId>jjwt-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>jjwt-demo</name>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>

		<!-- web模块 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
   
        <!-- jjwt依赖 -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</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>
	</dependencies>

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

</project

(3)token的创建

直接在测试类里写了。如下:

@SpringBootTest
class JjwtDemoApplicationTests {
     
    
    /*
     * 测试Token的生成
     */
	@Test
	public void createToken() {
     
	    //创建JwtBuilder对象
	    JwtBuilder jwtBuilder = Jwts.builder()
	                            .setId("0918")//声明的标识
	                            .setSubject("yanchengzhi")//主体
	                            .setIssuedAt(new Date())//创建日期
	                            //签名,第一个参数是算法,第二个参数是盐
	                            .signWith(SignatureAlgorithm.HS256, "likeyou");
	    //获取jwt的Token
	    String token = jwtBuilder.compact();
	    System.out.println(token);
	    System.out.println("--------------------------------------");
	    //分割
	    String []strs = token.split("\\.");
	    String header = Base64Codec.BASE64.decodeToString(strs[0]);
	    String payload = Base64Codec.BASE64.decodeToString(strs[1]);
	    //第三部分解析出来一定会乱码
	    String sign = Base64Codec.BASE64.decodeToString(strs[2]);
	    System.out.println("头部:" + header);
	    System.out.println("负载:" + payload);
	    System.out.println("签名:" + sign);
	}

}

执行这个方法,控制台:
在这里插入图片描述
将控制台的内容进行解析:
SpringSecurity_第195张图片
说明:sign部分解析出来是一定会乱码的,这正好说明了jwt的安全性非常好。

再次执行这个方法:
在这里插入图片描述
因为里面包含了时间,所以每次生成的jwt的token令牌都不一样。

(4)token的验证解析

已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

以下用代码测试token的解析,在测试类中添加:

	/*
	 *  测试Token的解析
	 */
	@Test
	public void parseToken() {
     
	    String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwOTE4Iiwic3ViIjoieWFuY2hlbmd6aGkiLCJpYXQiOjE2MTE1OTYzMjh9.shnFkp-39vyTxOfmTyexSDspCHP86RWTnsx-qsUkcK8";
	    //解析token,获取负载中的声明对象
	    Claims claims = Jwts.parser()
	                        .setSigningKey("likeyou")//这个就是秘钥
	                        .parseClaimsJws(token)
	                        .getBody();
	    //获取信息
	    String id = claims.getId();
	    String sub = claims.getSubject();
	    Date date = claims.getIssuedAt();
	    System.out.println("ID:" + id);
	    System.out.println("签发人:" + sub);
	    System.out.println("签发时间:" + date);
	}

执行这个方法,控制台:
在这里插入图片描述
(5)token过期校验

有很多时候,我们并不希望签发的token是永久生效的(上节的token是永久的),所以我们可以为token添加一个过期时间。原因:从服务器发出的token,服务器自己并不做记录,就存在一个弊端就是,服务端无法主动控制某token的立刻失效。

直接修改createToken方法,修改后的代码如下:

    /*
     * 测试Token的生成
     */
	@Test
	public void createToken() {
     
	    //获取系统当前时间
	    long current = System.currentTimeMillis();
	    long expire = current + 1000 * 60;
	    //创建JwtBuilder对象
	    JwtBuilder jwtBuilder = Jwts.builder()
	                            .setId("0918")//声明的标识
	                            .setSubject("yanchengzhi")//主体
	                            .setIssuedAt(new Date())//创建日期
	                            //签名,第一个参数是算法,第二个参数是盐
	                            .signWith(SignatureAlgorithm.HS256, "likeyou")
	                            .setExpiration(new Date(expire));//设置过期时间,1分钟后失效
	    //获取jwt的Token
	    String token = jwtBuilder.compact();
	    System.out.println(token);
	    System.out.println("--------------------------------------");
	    //分割
	    String []strs = token.split("\\.");
	    String header = Base64Codec.BASE64.decodeToString(strs[0]);
	    String payload = Base64Codec.BASE64.decodeToString(strs[1]);
	    //第三部分解析出来一定会乱码
	    String sign = Base64Codec.BASE64.decodeToString(strs[2]);
	    System.out.println("头部:" + header);
	    System.out.println("负载:" + payload);
	    System.out.println("签名:" + sign);
	}

执行,控制台:
在这里插入图片描述
然后将控制台生成的token替换掉parseToken方法中的token,执行parseToken这个方法,控制台:
在这里插入图片描述
一分钟后再执行这个方法:
SpringSecurity_第196张图片
报错了,提示jwt令牌过期。

(6)自定义claims

如果想存储更多的信息(例如角色),我们可以定义自定义claims。

直接修改createToken,修改后的如下:

	@Test
	public void createToken() {
     
	    //获取系统当前时间
	    long current = System.currentTimeMillis();
	    long expire = current + 1000 * 60;
	    //创建JwtBuilder对象
	    JwtBuilder jwtBuilder = Jwts.builder()
	                            .setId("0918")//声明的标识
	                            .setSubject("yanchengzhi")//主体
	                            .setIssuedAt(new Date())//创建日期
	                            //签名,第一个参数是算法,第二个参数是盐
	                            .signWith(SignatureAlgorithm.HS256, "likeyou")
	                            .setExpiration(new Date(expire))//设置过期时间,1分钟后失效
	                            .claim("name", "ycz")//自定义claims
	                            .claim("age", 25)
	                            .claim("like", "uuuu")
	                            .claim("wantto", "重庆");
	    //获取jwt的Token
	    String token = jwtBuilder.compact();
	    System.out.println(token);
	    System.out.println("--------------------------------------");
	    //分割
	    String []strs = token.split("\\.");
	    String header = Base64Codec.BASE64.decodeToString(strs[0]);
	    String payload = Base64Codec.BASE64.decodeToString(strs[1]);
	    //第三部分解析出来一定会乱码
	    String sign = Base64Codec.BASE64.decodeToString(strs[2]);
	    System.out.println("头部:" + header);
	    System.out.println("负载:" + payload);
	    System.out.println("签名:" + sign);
	}

执行,控制台:
SpringSecurity_第197张图片
或者这样写也行:

	@Test
	public void createToken() {
     
	    //获取系统当前时间
	    long current = System.currentTimeMillis();
	    long expire = current + 1000 * 60;
	    Map<String,Object> map = new HashMap<>();
	    map.put("name", "ycz");
	    map.put("age", 25);
	    map.put("like", "uuuu");
	    map.put("wantto", "重庆");
	    //创建JwtBuilder对象
	    JwtBuilder jwtBuilder = Jwts.builder()
	                            .setId("0918")//声明的标识
	                            .setSubject("yanchengzhi")//主体
	                            .setIssuedAt(new Date())//创建日期
	                            //签名,第一个参数是算法,第二个参数是盐
	                            .signWith(SignatureAlgorithm.HS256, "likeyou")
	                            .setExpiration(new Date(expire))//设置过期时间,1分钟后失效
	                            .addClaims(map);
	    //获取jwt的Token
	    String token = jwtBuilder.compact();
	    System.out.println(token);
	    System.out.println("--------------------------------------");
	    //分割
	    String []strs = token.split("\\.");
	    String header = Base64Codec.BASE64.decodeToString(strs[0]);
	    String payload = Base64Codec.BASE64.decodeToString(strs[1]);
	    //第三部分解析出来一定会乱码
	    String sign = Base64Codec.BASE64.decodeToString(strs[2]);
	    System.out.println("头部:" + header);
	    System.out.println("负载:" + payload);
	    System.out.println("签名:" + sign);
	}

推荐使用这种,将全部信息放到map集合,然后入参这个map就行了。执行,控制台:
在这里插入图片描述
然后解析的时候将这些内容获取出来,修改parseToken,如下:

	/*
	 *  测试Token的解析
	 */
	@Test
	public void parseToken() {
     
	    String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIwOTE4Iiwic3ViIjoieWFuY2hlbmd6aGkiLCJpYXQiOjE2MTE1OTkwMjgsImV4cCI6MTYxMTU5OTA4OCwid2FudHRvIjoi6YeN5bqGIiwibGlrZSI6InV1dXUiLCJuYW1lIjoieWN6IiwiYWdlIjoyNX0.cjyqcgJowoYYwyVhJdloqraYJ4FgVqMXfhZzPp9FyVA";
	    //解析token,获取负载中的声明对象
	    Claims claims = Jwts.parser()
	                        .setSigningKey("likeyou")//这个就是秘钥
	                        .parseClaimsJws(token)
	                        .getBody();
	    //获取信息
	    String id = claims.getId();
	    String sub = claims.getSubject();
	    Date date = claims.getIssuedAt();
	    String name = (String) claims.get("name");
	    int age = (Integer) claims.get("age");
	    String like = (String) claims.get("like");
	    String want = (String) claims.get("wantto");
	    System.out.println("ID:" + id);
	    System.out.println("签发人:" + sub);
	    System.out.println("签发时间:" + date);
	    System.out.println("姓名:" + name);
	    System.out.println("年龄:" + age);
	    System.out.println("喜欢的人:" + like);
	    System.out.println("想要去的地方:" + want);
	}

执行,控制台:
SpringSecurity_第198张图片

6、SpringSecurity + OAuth2.0 + JWT

前面只使用Oauth2.0的话,颁发的通行令牌长度太短了,现在想整合JWT,将颁发的token转换一下,转换成jwt格式的长令牌。

6.1、整合JWT

直接在此工程的基础上修改了。
SpringSecurity_第199张图片
(1)pom

注释掉redis有关的依赖。
SpringSecurity_第200张图片
(2)Redis配置类

注释掉Redis的配置类。
SpringSecurity_第201张图片
(3)Jwt配置类

在config包下创建JwtTokenStoreConfig配置类,如下:

/*
 * Jwt配置类
 */
@Configuration
public class JwtTokenStoreConfig {
     
    
    //注册JwtAccessTokenConverter
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
     
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //配置jwt秘钥
        jwtAccessTokenConverter.setSigningKey("yanchengzhi");
        return jwtAccessTokenConverter();
    }
    
    //注册TokenStore
    @Bean
    public TokenStore jwtTokenStore() {
     
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

}

(4)修改授权配置类

修改AuthorizationServerConfig类,如下:
SpringSecurity_第202张图片
标记的地方是添加或修改的。

(5)测试

使用密码模式获取jwt令牌,如下:
SpringSecurity_第203张图片
返回结果如下:
SpringSecurity_第204张图片
现在的access_token令牌的长度发生了变化,与它对应的是jti值。解析这个token值:
SpringSecurity_第205张图片
没问题,解析的结果也是正确的。

6.2、扩展JWT的内容

现在想往JWT令牌中添加自定义的内容,过程如下。

(1)Jwt内容增强器

创建一个jwt包,包下创建一个Jwt的内容增强器JwtTokenEnhancer,如下:

/*
 * Jwt内容增强器
 * 需要实现TokenEnhancer接口
 * 
 */
public class JwtTokenEnhancer implements TokenEnhancer{
     

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, 
            OAuth2Authentication authentication) {
     
        //自定义的内容存到map中
        Map<String,Object> map = new HashMap<>();
        map.put("address","湖北");
        map.put("like","uuuu");
        map.put("age",25);
        map.put("qq昵称","云过梦无痕");
        //下转型
        if(accessToken instanceof DefaultOAuth2AccessToken) {
     
            DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken)accessToken;
            defaultOAuth2AccessToken.setAdditionalInformation(map);
            return defaultOAuth2AccessToken;
        }
        return null;
    }

}

(2)修改Jwt配置类

修改Jwt配置类JwtTokenStoreConfig,如下:
SpringSecurity_第206张图片
标记的是新添加的内容。

(3)修改授权服务配置

修改AuthorizationServerConfig类,如下:
SpringSecurity_第207张图片
标记的是新添加的内容。

(4)测试

启动工程,使用postman进行测试:
SpringSecurity_第208张图片
发送请求,返回结果如下:
SpringSecurity_第209张图片
access_token令牌长度变长了,而且下面是我添加的自定义的内容。解析生成的jwt令牌:
SpringSecurity_第210张图片
令牌中包含添加的自定义内容,但是后面的乱码了。

6.3、解析JWT的内容

JWT令牌的内容一般要在java程序中解析出来,以下演示过程。

(1)pom依赖

还是使用jjwt来解析。pom中添加依赖:

         <!-- jjwt依赖 -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</dependency>

(2)controller

修改UserController,如下:

@RestController
@RequestMapping("/user")
public class UserController {
     
    
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication,HttpServletRequest request) {
     
        //获取请求头的指定内容
        String header = request.getHeader("Authorization");
        //截取,去掉请求头的前6位,获取token
        String token = header.substring(header.indexOf("bearer") + 7);
        //解析Token,获取Claims对象
        Claims claims = Jwts.parser()
                            .setSigningKey("yanchengzhi".getBytes(StandardCharsets.UTF_8))
                            .parseClaimsJws(token)
                            .getBody();
        return claims;
    }

}

(3)测试

先获取jwt令牌:
SpringSecurity_第211张图片
SpringSecurity_第212张图片
然后带着jwt令牌去获取资源:
SpringSecurity_第213张图片
请求头Header里面带一个参数Authorization,值为bearer后面加一个空格,然后跟jwt令牌内容,发送,返回结果:
SpringSecurity_第214张图片
获取到了,返回的是jwt令牌解析后的内容。

6.4、JWT刷新令牌

在Spring Cloud Security中使用oauth2时,如果令牌失效了,可以使用刷新令牌通过refresh_token的授权模式再次获取access_token,只需修改认证服务器的配置,添加refresh_token的授权模式即可。

修改授权服务配置类AuthorizationServerConfig,如下:
SpringSecurity_第215张图片
给jwt通行令牌加一个有效期,这里有效期设为1分钟。启动工程进行测试:
SpringSecurity_第216张图片
SpringSecurity_第217张图片
现在jwt通行令牌还有效,1分钟后再发请求:
SpringSecurity_第218张图片
获取不到资源了,现在jwt通行令牌已经过期了。解决方法是加一个刷新令牌refresh_token。如下:
SpringSecurity_第219张图片
授权模式里面添加一个refresh_token并且设置刷新令牌的有效期。然后再启动工程获取令牌:
SpringSecurity_第220张图片
现在返回结果里面多了一个refresh_token,这个就是刷新令牌。等1分钟,用通行令牌获取资源:
SpringSecurity_第221张图片
通行令牌过期了。现在使用刷新令牌直接从授权服务端获取新的通行令牌:
SpringSecurity_第222张图片
SpringSecurity_第223张图片
表单里面带2个参数:grant_type的值为refresh_token,refresh_token的值为刚才返回的刷新令牌。发送:
SpringSecurity_第224张图片
获取到了新的通行令牌和刷新令牌。同样的,通行令牌的有效期还是1分钟,刷新令牌是1小时,再用这个新的通行令牌获取资源:
SpringSecurity_第225张图片
资源获取成功。

7、SpringSecurity + OAuth2.0 + JWT整合SSO

7.1、SSO单点登录

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

如下图:
SpringSecurity_第226张图片
如上图,假设有4个子系统。现在用户首次登录子系统1:

  • 用户使用浏览器登录子系统1,子系统1请求认证系统对用户的身份进行认证。
  • 认证通过后,认证服务会返回一个token给浏览器,浏览器一般将这个token存在Cookie中。
  • 用户继续在浏览器中登录子系统2,那么这时会带着Cookie中的token直接去认证系统进行认证。
  • 认证系统认证通过,用户无需再次输入账号密码登录,可以直接进入子系统2。登录其他子系统也是一样的。

通过以上过程可以看到,用户只需要成功登录一次,就可以不用再进行登录而直接进入其他子系统。

7.2、测试

(1)创建工程

创建一个新的工程模拟子系统:
SpringSecurity_第227张图片
(2)pom依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
	https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.8.RELEASE</version>
		<relativePath /> 
	</parent>
	
	<groupId>com.ycz</groupId>
	<artifactId>system1</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>system1</name>

	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
	</properties>

	<dependencies>

		<!-- spring cloud中的oauth2依赖 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
		</dependency>

		<!-- spring cloud中的security依赖 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-security</artifactId>
		</dependency>

		<!-- web模块 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
         
         <!-- jjwt依赖 -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.9.0</version>
		</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>
	</dependencies>

	<!-- 引入spring cloud -->
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${
     spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

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

</project>

(3)yml配置

application.yml的内容如下:

server:
  port: 9900
  ##防止Cookie冲突
  servlet:
    session:
      cookie:
        name: session01
  
## 授权服务地址
oauth2-server-url: http://localhost:9999

security:
  oauth2:
    ## 客户端配置
    client:
      client-id: client ## 客户端ID
      client-secret: ycz123456  ## 密码
      user-authorization-uri: ${
     oauth2-server-url}/oauth/authorize  ## 获取授权码的url
      access-token-uri: ${
     oauth2-server-url}/oauth/token  ## 获取access_token的url
    
    ## 服务端配置
    resource:
      jwt:
        ## 获取jwt令牌的url
        key-uri: ${
     oauth2-server-url}/oauth/token_key 

(4)开启sso功能

启动类上加@EnableOAuth2Sso注解,如下:

@SpringBootApplication
//开启sso单点登录功能
@EnableOAuth2Sso
public class System1Application {
     

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

}

(5)controller

controller包下创建UserController,如下:

@RestController
@RequestMapping("/user")
public class UserController {
     
    
    @GetMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
     
        return authentication.getPrincipal();
    }

}

(6)修改认证服务器的配置

修改AuthorizationServerConfig类,如下:
SpringSecurity_第228张图片
修改重定向的uri,下面添加的。

同时启动system1工程和springsecurity-oauth2-demo工程。浏览器访问如下:
在这里插入图片描述
在这里插入图片描述
需要认证服务的认证输入用户名和密码,登录:
SpringSecurity_第229张图片
成功获取资源,再看调试工具:
SpringSecurity_第230张图片
SpringSecurity_第231张图片
成功写到了Cookie中。

(7)添加一个子系统

复制system1系统,重命名system2如下:
SpringSecurity_第232张图片
只修改yml:
SpringSecurity_第233张图片
如上,改了端口和Cookie的名称。

再修改一下跳转的uri:
SpringSecurity_第234张图片
添加了一个跳转的uri。

重新启动这3个工程,访问如下:
SpringSecurity_第235张图片
跳到了登录页面:
SpringSecurity_第236张图片
输入账号密码登录:
SpringSecurity_第237张图片
成功获取到了资源。

再通过第二个子系统访问资源:
SpringSecurity_第238张图片
SpringSecurity_第239张图片
成功访问到了资源。可以看到子系统2并没有再进行登录,而是直接可以访问资源,这就是单点登录SSO,只用成功登录一次便可以在所有子系统中通行。再看Cookie:
SpringSecurity_第240张图片
成功的添加了一个名称为session02的Cookie。

8、总结

通过学习,掌握Spring Security、Oauth2.0、JWT的用法。以及整合Spring Security + Oauth2.0 + JWT进行SSO单点登录。

你可能感兴趣的:(Spring)