SpringSecurity4 学习笔记

成结构

image-20210611180039216

1. SpringSecurity

image-20210701171058255

2. 入门案例

新建SpringBoot项目 添加Security和Web依赖即可

在resources–static新建登录和主页简单静态页面

image-20210701190750962

启动项目 访问localhost:8080/login可以看到出现的 登录页(该页面为Security默认生成的)

image-20210701190908233

在启动项目时 会在控制台输出一串登录密码

image-20210701190951245

2.1 UserDetailsService接口

image-20210701191303883

用户判断用户名是否存在

而UserDetailsService返回的Userdetails接口中实现了以下方法

image-20210701191627376

查看UserDetails的实现类可以看到User的类

image-20210701191828655

image-20210701192431298

本质就是 通过UserDetialsService中的loadUserByUsername去数据库查询 查询后返回UserDetails 实现的User中 用户名,密码,权限信息

2.2 passwordEncoder接口

​ BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。

​ BCryptPasswordEncoder是对 bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10.

image-20210701193236160

密码的解析器

在它的实现类中可以看到很多的实现类 其中官方推荐使用BCryptPasswordEncoder

image-20210701193417903

测试

@Test
void contextLoads() {
    BCryptPasswordEncoder pw = new BCryptPasswordEncoder();

    //加密
    String encode = pw.encode("123");
    System.err.println(encode);

    //比较密码
    boolean matches = pw.matches("123", encode);
    System.err.println("==============");
    System.err.println(matches);
}

image-20210701193720169

加入了随机的盐Salt(相当于是一个随机的字符串) 使得每次加密后的密文不同

2.3 自定义登录逻辑

当进行自定义登录逻辑时需要用到之前讲解的UserDetailsServicePasswordEncoder。但是Spring Security要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder实例。所以不能直接new对象。

编写SecurityConfig配置类

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

    @Bean
    public PasswordEncoder getPw(){
        return new BCryptPasswordEncoder();
    }
}

编写实现类

UserServiceImpl.java

public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //1. 根据用户名去数据库查询,如果不存在抛UsernameNotFoundException异常
        if (!"admin".equals(username)){
            throw new UsernameNotFoundException("用户名不存在");
        }

        //2. 比较密码(注册时已经加密过),如果匹配成功返回UserDetails
        String password = passwordEncoder.encode("123");

        return new User(username,password,
                        AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
    }
}

2.4 自定义登录页面

  1. 登录页 login.html
<!doctype html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>登陆界面</title>
    <style>
    html {
    background-color: #B5DEF2;
}

.wrapper {
    margin: 140px 0 140px auto;
    width: 884px;
}

.loginBox {
    background-color: #F0F4F6;
    /*上divcolor*/
    border: 1px solid #BfD6E1;
    border-radius: 5px;
    color: #444;
    font: 14px 'Microsoft YaHei', '微软雅黑';
    margin: 0 auto;
    width: 388px
}

.loginBox .loginBoxCenter {
    border-bottom: 1px solid #DDE0E8;
    padding: 24px;
}

.loginBox .loginBoxCenter p {
    margin-bottom: 10px
}

.loginBox .loginBoxButtons {
    /*background-color: #F0F4F6;*/
    /*下divcolor*/
    border-top: 0px solid #FFF;
    border-bottom-left-radius: 5px;
    border-bottom-right-radius: 5px;
    line-height: 28px;
    overflow: hidden;
    padding: 20px 24px;
    vertical-align: center;
    filter: alpha(Opacity=80);
    -moz-opacity: 0.5;
    opacity: 0.5;
}

.loginBox .loginInput {
    border: 1px solid #D2D9dC;
    border-radius: 2px;
    color: #444;
    font: 12px 'Microsoft YaHei', '微软雅黑';
    padding: 8px 14px;
    margin-bottom: 8px;
    width: 310px;
}

.loginBox .loginInput:FOCUS {
    border: 1px solid #B7D4EA;
    box-shadow: 0 0 8px #B7D4EA;
}

.loginBox .loginBtn {
    background-image: -moz-linear-gradient(to bottom, blue, #85CFEE);
    border: 1px solid #98CCE7;
    border-radius: 20px;
    box-shadow: inset rgba(255, 255, 255, 0.6) 0 1px 1px, rgba(0, 0, 0, 0.1) 0 1px 1px;
    color: #444;
    /*登录*/
    cursor: pointer;
    float: right;
    font: bold 13px Arial;
    padding: 10px 50px;
}

.loginBox .loginBtn:HOVER {
    background-image: -moz-linear-gradient(to top, blue, #85CFEE);
}

.loginBox a.forgetLink {
    color: #ABABAB;
    cursor: pointer;
    float: right;
    font: 11px/20px Arial;
    text-decoration: none;
    vertical-align: middle;
    /*忘记密码*/
}

.loginBox a.forgetLink:HOVER {
    color: #000000;
    text-decoration: none;
    /*忘记密码*/
}

.loginBox input#remember {
    vertical-align: middle;
}

.loginBox label[for="remember"] {
    font: 11px Arial;
}
</style>
    </head>
    <body>
    <div class="wrapper">
        <form action="/login" method="post">
        <div class="loginBox">
            <div class="loginBoxCenter">
                <p><label>用户名:</label></p>
                <!--autofocus 规定当页面加载时按钮应当自动地获得焦点。 -->
                <!-- placeholder提供可描述输入字段预期值的提示信息-->
                <p><input type="text" id="text" name="username" class="loginInput" autofocus="autofocus" required="required" autocomplete="off" placeholder="请输入邮箱/手机号" value="" /></p>
                    <!-- required 规定必需在提交之前填写输入字段-->
                    <p><label>密码:</label></p>
                    <p><input type="password" id="password" name="password" class="loginInput" required="required" placeholder="请输入密码" value="" /></p>
                        <p><a class="forgetLink" href="#">忘记密码?</a></p>
                            <input id="remember" type="checkbox" name="remember" />
                            <label for="remember">记住登录状态</label>
                                </div>
                                <div class="loginBoxButtons">


                                    <button class="loginBtn">登录</button>
                                        <div> 新用户注册</div>
                                        </div>
                                        </div>
                                        </form>
                                        </div>

                                        </body>
                                        </html>

表单提交为 action="/login"

并新建登录后跳转的主页面 main.html

  1. 更改SecurityConfig中权限信息

image-20210701194620073

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

    @Bean
    public PasswordEncoder getPw(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //设置表单提交
        http.formLogin()
            //自定义登陆页面
            .loginPage("/login.html")
            //必须和表单提交的接口action 一样,会去执行自定义登录逻辑
            .loginProcessingUrl("/login")
            //登录成功后跳转的页面 Post
            .successForwardUrl("/toMain");

        //授权
        http.authorizeRequests()
            //放行/login .html,不需要认立
            .antMatchers("/login.html").permitAll()
            //所有请求都必须认证才能访问,必须登录
            .anyRequest().authenticated();


        //关闭csrf防护
        http.csrf().disable();
    }
}
  1. controller跳转
@RequestMapping("/toMain")
public String main(){
    return "redirect:main.html";
}

2.5 自定义错误页面

在登录失败后 没有页面跳转 url变为image-20210702094933376

简单编写登录失败页面 error.html

image-20210702095540552

增加配置类信息

image-20210702095612563

添加controller跳转路径

@RequestMapping("/toError")
public String error(){
    return "redirect:error.html";
}

在配置类信息中对错误页面进行放行

image-20210702095714542

2.6 设置请求账户和密码的参数名

用户名密码过滤器UsernamePasswordAuthenticationFilter

image-20210702100951844

测试

  1. 将 页面参数改为想要传的值

    image-20210702101225204

  2. 配置类中添加配置信息 将用户名和密码对应

    image-20210702101333297

    默认的用户名和密码参数为 username和password

2.7 自定义登录成功(处理器)

如果想要登录成功后跳转到b站等页面 会提示报错

image-20210702103550727

查看.successForwardUrl("http://www.bilibili.com")中的successForwardUrl方法

image-20210702103736493

image-20210702103716199

自定义实现验证跳转AuthenticationSuccessHandler接口

  1. MyAuthenticationSuccessHandler.java
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    public MyAuthenticationSuccessHandler(String url) {
        this.url = url;
    }

    private String url;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect(url);
    }
}
  1. 配置登录成功后转发网址

    image-20210702104442083

在自定义的MyAuthenticationSuccessHandler类中重写的onAuthenticationSuccess方法 在验证参数中可以获取到用户的信息

image-20210702105016868

2.8 自定义登录失败(处理器)

和登录成功时类似

创建自定义接口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);    }}

修改配置类

image-20210702110112973

3. 授权

anyRequest 详解

在之前认证过程中我们就已经使用过anyRequest(),表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证。

image-20210702110609697

antMatchers 详解

方法定义如下

public C antMatchers(String... antPatterns)

参数是不定向参数, 每个参数是一个 ant表达式, 用于匹配URL规则

  • ?: 匹配一个字符
  • *: 匹配0个或多个字符
  • **: 匹配0个或多个目录

在实际项目中一般都是放行所有静态资源 例如 放行js文件下所有脚本文件

.antMatchers("/js/**","/css/**").permitAll()

或者还有一种匹配方法是只要是.js结尾都放行

.antMatchers("**/*.js").permitAll()

image-20210702111543561

regexMatchers 详解

image-20210702111844869

查看regexMatchers方法可以看到有两个实现方法

image-20210702112030392

两个参数的方法中的前面的method是一个枚举类 定义了请求类型

image-20210702112137831

image-20210702112411802

指定请求方法访问资源

mvcMatchers 详解

image-20210702112851573

3.1 内置权限的控制

image-20210702131034910

3.2 角色权限判断

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

hasAuthority(String) 权限

​ 判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建User对象时指定的。下图中 admin和normal 就是用户的权限。admin和normal 严格区分大小写。


  1. 配置权限

    //权限控制,严格区分大小写.antMatchers("/main1.html").hasAnyAuthority("admin")    //设置多个权限 用户如果拥有前面第一个权限admin未拥有后面的 同样可以访问    .antMatchers("/main1.html").hasAnyAuthority("admin","amd")
    

    在最开始定义用户的时候给admin用户了admin权限 当权限改为未授权的权限则显示403

image-20210702132518051

hasRole(String) 角色

​ 判断用户是否为该角色

  1. 在UserServiceImpl中添加用户角色

    image-20210702133235200

    ROLE_开头会自动将abc设置为该角色

    //访问该路径 需要abc1角色.antMatchers("/main1.html").hasRole("abc1")//设置多个角色 只要有一个角色想匹配就可访问.antMatchers("/main1.html").hasAnyRole("abc","aaa")
    

hasIpAddress(String) IP地址

根据IP判断访问权限

.antMatchers("/main1.html").hasIpAddress("127.0.0.2")

3.3 自定义403处理方案

  1. 由于403是因为权限不足 所以 自定义MyAccessDeniedHandler并实现AccessDeniedHandler接口

    @Componentpublic class MyAccessDeniedHandler implements AccessDeniedHandler {    @Override    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {        //响应状态码        response.setStatus(HttpServletResponse.SC_FORBIDDEN);        //返回JSON格式        response.setHeader("Content-Type","application/json;charset=utf-8");        PrintWriter writer = response.getWriter();        writer.write("{\"state\":\"error\",\"msg\":\"权限不足\"}");        writer.flush();        writer.close();    }}
    

    注意加上@Component注解 让spring进行管理

  2. 在配置类中进行配置

    //首先注入该类@AutowiredMyAccessDeniedHandler myAccessDeniedHandler;//配置该方案http.exceptionHandling()              .accessDeniedHandler(myAccessDeniedHandler);
    
  3. 测试访问

    当权限不足时 会出现自定义的JSON字符串

    image-20210702135405536

3.4 基于表达式的访问控制

access() 方法使用

之前所学习的登录用户权限判断 实际上底层实现都是调用access(表达式)

image-20210702135945029

可以通过access()实现和之前权限控制完成相同的功能

和之前写法类似

.antMatchers("/main1.html").access("hasRole('abc')")

3.4.1 自定义access()方法
  1. 创建 MyService接口

    public interface MyService {    boolean hasPermission(HttpServletRequest request, Authentication authentication);}
    
  2. 创建实现类

    @Servicepublic class MyServiceImpl implements MyService {    @Override    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {        //获取主体数据        Object obj = authentication.getPrincipal();        //instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。        //判断主体是否属于UserDetails        if (obj instanceof UserDetails){            //获取对应的权限            UserDetails userDetails = (UserDetails) obj;            Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();            //判断请求的URI是否在权限里            return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));        }        return false;    }}
    
  3. 配置权限

    image-20210702143457622

  4. 测试

    image-20210702143511183

    是因为登录跳转后跳转页面 所获取的URI为 /main.html 所以需要在UserServiceImpl中给该用户添加配置权限

    image-20210702143736607

3.4.2 基于注解的访问控制

​ 在Spring Security中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity进行开启后使用。

​ 如果设置的条件允许,程序正常执行。如果不允许会报500

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

@Secured

@Secured 是专门用于判断是否具有角色的, 能写在方法或类上, 参数以ROLE_开头

image-20210702144456883

  1. 开启注解

    在启动类上加上@EnableGlobalMethodSecurity(securedEnabled = true)

  2. 在控制器方法上添加@Secured注解

    @RequestMapping("/toMain")@Secured("ROLE_abc")public String main(){return "redirect:main.html";}
    

@PreAuthorized (使用比较多)/@PostAuthorize

@PreAuthorized/@PostAuthorize 都是方法或类级别注解

image-20210702150434793

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

  2. 在控制器方法上添加注解@PreAuthorize

    @PreAuthorize("hasRole('abc')")public String main(){    return "redirect:main.html";}
    

3.5 RememberMe功能实现

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

添加依赖

​ Spring Security实现Remember Me功能时底层实现依赖Spring-JDBC,所以需要导入Spring-JDBC。以后多使用MyBatis框架而很少直接导入spring-jdbc,所以此处导入mybatis启动器同时还需要添加MySQL驱动

 <!--mybatis--><dependency>    <groupId>org.mybatis.spring.boot</groupId>    <artifactId>mybatis-spring-boot-starter</artifactId>    <version>2.1.3</version></dependency><dependency>    <groupId>mysql</groupId>    <artifactId>mysql-connector-java</artifactId>    <version>8.0.25</version></dependency>

编写数据源信息

spring:	datasource:        driver-class-name: com.mysql.cj.jdbc.Driver        url: jdbc:mysql://localhost:3306/study_demo?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai        username: root        password: 123456

添加SecurityConfig配置类

http.rememberMe().tokenrRepository()这里在tokenrRepository()中需要一个对象

image-20210702154224228

查看这是一个接口 点击实现类可知

image-20210702154333619

选用jdbc

在配置类中添加

image-20210702155845656

设置记住我

image-20210702155907644

设置页面记住我 name属性值为 remember-me

image-20210702160658265

可以查看到绑定的数据库已经自动生成了表

image-20210702160122134

表生成后 需要把配置中的自动创建表设置给关掉

4. 在Thymeleaf中使用

​ Spring Securitl可以在一些视图技术中进行控制显示效果。例如: JSPThymeleaf。在非前后端分离且使用Spring Boot的项目中多使用Thymeleaf 作为视图展示技术。

​ Thymeleaf对Spring Security的支持都放在thymeleaf-extras-springsecurityX中,目前最新版本为5。所以需要在项目中添加此jar包的依赖和thymeleaf 的依赖。。

<!--thymeleaf springSecurity5--><dependency>    <groupId>org.thymeleaf.extras</groupId>    <artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency><dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>

在html页面引入 Thymeleaf 命名空间和security命名空间

<html lang="en" xmlns="http://www.w3.org/1999/xhtml"                xmlns:th="http://www.thymeleaf.org"                xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

测试

在templates目录下创建模板

<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"      xmlns:th="http://www.thymeleaf.org"      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"><head>    <meta charset="UTF-8">    <title>Title</title></head><body>登录账号: <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>

添加视图控制

@GetMapping("/demo")public String demo(){    return "demo";}

image-20210702165908536

image-20210702170000325

5. 退出登录

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

在页面中添加/logout的超链接即可

<a href="/logout">退出登录</a>

为了实现更好的效果,通常添加退出的配置。默认的退出url为/logout,退出成功后跳转到/login?logout

image-20210702171412628

退出后 连接后有?logout 如果想要去掉 则需要进行配置

http.logout()    .logoutSuccessUrl("/login.html");

6. CSRF

之前在配置类中一直存着这样一行代码 http.csrf.disable() 如果没有这行代码导致用户无法被认证,这行代码的含义是, 关闭csrf防护

6.1 什么是CSRF

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

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

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

Spring Security中的CSRF

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

测试

  1. 关闭csrf 新建登录页面模板

  2. 在表单中加入_csrf

    image-20210704184948042

  3. 配置Controller

    @GetMapping("/showLogin")public String showLogin(){    return "login";}
    
  4. 在SecurityConfig中将showConfig放行

登录成功后 可见请求中Headers中携带了_csrf

image-20210704185424344

7. Oauth2认证

介绍

​ 第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。

​ OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。

​ Oauth协议目前发展到2.o版本,1.o版本过于复杂,2.o版本已得到广泛应用。

img

Oauth2 协议认证流程

image-20210704192852966

角色

客户端

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

资源拥有者

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

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

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

资源服务器

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

常用术语

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

令牌类型

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

优点

更安全,客户端不接触用户密码,服务器端更易集中保护广泛传播并被持续采用

短寿命和封装的token

资源服务器和授权服务器解耦集中式授权,简化客户端

HTTP/JSON友好,易于请求和传递token考虑多种客户端架构场景

客户可以具有不同的信任级别

缺点

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

授权模式

  • 授权码模式

    img

  • 简化授权模式

    img

  • 密码模式

    img

  • 客户端模式

    image-20210704222100390

  • 刷新令牌

    image-20210704222353225

8. SpringSecurity Oauth2

授权服务器

image-20210704223105995

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

Spring Security Oauth2架构

img

8.1 Spring Security Oauth2 授权码模式

  1. 创建项目 引入依赖

这边springboot版本用的<version>2.1.3.RELEASE</version>

版本不对应的话 项目会跑不起来

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
   <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
   </dependency>

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

   <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
   </dependency>

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

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

 <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>

创建User pojo实体类 实现 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;
    }
}

创建UserService 实现UserDetailsService接口

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        String password = passwordEncoder.encode("123456");
        return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

    }
}

配置SecurityConfig信息

@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
                .csrf().disable();

    }

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

为了演示方便将 资源服务器和授权服务器放在一起配置演示

配置授权服务器

public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //为了演示方便先放在内存中, 实际开发不是如此
        clients.inMemory()
                //客户端ID
                .withClient("client")
                //密钥
                .secret(passwordEncoder.encode("112233"))
                //重定向地址
                .redirectUris("http://www.baidu.com")
                //授权范围
                .scopes("all")
                /**
                 * 授权类型
                 * authorization_code 授权码模式
                 */
                .authorizedGrantTypes("authorization_code");
    }
}

配置资源服务器

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

配置Controller

@RestController
@RequestMapping("/user")
public class UserController {

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

测试

  • 获取授权码

http://localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all

由于定义的客户端id为client所以将 client_id=admin改为client

登录后进入 跳转服务器

image-20210705111434620

选择允许后跳转到指定页面

image-20210705111607910

得到令牌code=t0k7WA

根据授权码获取令牌(Post请求)

  1. 获取token

image-20210705111909499

image-20210705112406942

body中需要填加的信息

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

认证失败服务端会返回 401

  1. 使用token获取当前用户信息

image-20210705120959483

8.2 密码模式

  • 在SecurityConfig中配置认证管理器

    @Bean
    public AuthenticationManager authenticationManager() throws 		Exception {
        return super.authenticationManager();
    }
    
  • 在授权服务配置AuthorizationServerConfig中重写配置

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserService userService;
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            //为了演示方便先放在内存中, 实际开发不是如此
            clients.inMemory()
                    //客户端ID
                    .withClient("client")
                    //密钥
                    .secret(passwordEncoder.encode("112233"))
                    //重定向地址
                    .redirectUris("http://www.baidu.com")
                    //授权范围
                    .scopes("all")
                    /**
                     * 授权类型   (可以支持多个模式同时使用)
                     * authorization_code 授权码模式
                     * password 密码模式
                     */
                    .authorizedGrantTypes("authorization_code","password");
        }
    
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
                endpoints.authenticationManager(authenticationManager)
                        .userDetailsService(userService);
        }
    }
    

    测试

    image-20210705131907158

    image-20210705131834549

    发送得到新的令牌

    image-20210705131938844

    将token添加 请求得到

    image-20210705132101806

8.3 在Redis中存储token

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

1. 添加依赖及配置

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.1.9.RELEASE</version>
</dependency>

<!--commons-pool2 对象池依赖-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

在Application中配置redis配置信息

# Redis
spring:
  redis:
    host: 127.0.0.1

2. 创建RedisConfig

@Configuration
public class RedisConfig {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}

3. 在AuthorizationServerConfig中的密码模式里进行配置

@Autowired
@Qualifier("redisTokenStore")
private TokenStore tokenStore;

/**
 * 密码模式
 * @param endpoints
 * @throws Exception
 */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userService)
                    .tokenStore(tokenStore);
    }

4. 在postman中以密码模式做测试发送请求

image-20210705134036950

在Redis中可以看到

image-20210705134113366

9. JWT

9.1 常见的认证机制

HTTP Basic Auth

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

Cookie Auth

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

OAuth

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

​ OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

Token Auth

便用基于Token的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

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

image-20210705135143350

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

具体: Token Auth的有点(Token机制相对于Cookie机制又有什么好处?):

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

9.2 JWT简介

什么是JWT

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

JWT令牌的优点:

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

缺点:

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

JWT组成

一个JWT实际上是一个字符串 它由三部分组成, 头部、载荷、签名

1. 头部(Headers):

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

{
    "alg":"HS256",
    "typ":"JWT"
}
  • typ: 是类型
  • alg: 签名的算法,这里使用的算法是HS256算法

我们对头部的json字符串进行BASE64编码

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK中提供了非常方便的 BASE64EncoderBASE64Decoder,用它们可以非常方便的完成基于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进行验证以及规则才行。

{
    "sub":"123456",
    "name":"ahui",
    "iat":"12133333",
}

其中sub是标准的声明, name是自定义的声明(公共的或私有的)

然后将其进行base64编码, 得到jwt的第二部分

声明中不要放一些敏感信息

3. 签证、签名(signature)

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

  • header(base64后的)

  • payload(base64后的)

  • secret(盐, 一定要保密)

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

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

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

9.3 JJWT简介

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

image-20210705143527961

创建项目测试使用JJWT生成JWT

1. 创建新的boot项目用于测试

导入pom依赖

<dependencies>
        <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>

        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
</dependencies>

创建测试类生成jwt

/**
     * 生成jwt
     */
    @Test
    public void testJwt() {
        JwtBuilder jwtBuilder = Jwts.builder()
                //唯一ID{"id":"314"}
                .setId("314")
                //接收的用户 {"sub":"Ahui"}
                .setSubject("Ahui")
                //签发时间 {"iat":"...."}
                .setIssuedAt(new Date())
                //签名算法,及密钥
                .signWith(SignatureAlgorithm.HS256, "xxxx");
        //签发token
        String token = jwtBuilder.compact();
        System.err.println(token);
    }

image-20210705150309329

在jwt解密可得到

image-20210705150400334

或者使用分隔 base64反编码解密

image-20210705150750307


image-20210705150801809

token的验证解析

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

/**
 * 解析token
 */
    @Test
    public void testParseToken(){
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIzMTQiLCJzdWIiOiJBaHVpIiwiaWF0IjoxNjI1NDY4ODM0fQ.aH0QUMEQwyeZc6Ab3cBzckF5t3TC-3KnvRjPJu_XtiI";
        Claims claims = (Claims) Jwts.parser()
                //密钥一定要和签发时设置的一致
                .setSigningKey("xxxx")
                .parse(token)
                .getBody();
        System.err.println("id=" + claims.getId());
        System.err.println("sub=" + claims.getSubject());
        System.err.println("iat=" + claims.getIssuedAt());
    }
}

设置token过期时间

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

测试:

/**
     * 生成jwt (有过期时间  开始时间)
     */
    @Test
    public void testJwtHasExpire() {
        //当前时间
        long date = System.currentTimeMillis();
        //失效时间
        long exp = date + 60 * 1000;
        JwtBuilder jwtBuilder = Jwts.builder()
                //唯一ID{"id":"314"}
                .setId("314")
                //接收的用户 {"sub":"Ahui"}
                .setSubject("Ahui")
                //签发时间 {"iat":"...."}
                .setIssuedAt(new Date())
                //签名算法,及密钥
                .signWith(SignatureAlgorithm.HS256, "xxxx")
                //设置失效时间
                .setExpiration(new Date(exp));
        //签发token
        String token = jwtBuilder.compact();
        System.err.println(token);


        System.err.println("=================");
        String[] split = token.split("\\.");
        System.err.println(Base64Codec.BASE64.decodeToString(split[0]));
        System.err.println(Base64Codec.BASE64.decodeToString(split[1]));
        //第三部分会乱码
        System.err.println(Base64Codec.BASE64.decodeToString(split[2]));
    }

相当于添加了过期时间配置

//当前时间
long date = System.currentTimeMillis();
//失效时间
long exp = date + 60 * 1000;

//设置失效时间
.setExpiration(new Date(exp));

一分钟内可以解析, 一分钟后 会抛异常

image-20210705153825059

自定义申明(Claims)

在配置中添加自定义claim即可

image-20210705154455172

解码后获取

image-20210705154513224

image-20210705154523601

10. SpringSecurityOauth2 集成JWT

使用之前继承redis的项目进行测试

由于jwt是无状态的 所以不需要redis 所以先将依赖以及配置删除测试

1. 添加jwt配置信息

JwtTokenStoreConfig.java

@Configuration
public class JwtTokenStoreConfig {

    @Bean
    public TokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //设置jwt密钥
        jwtAccessTokenConverter.setSigningKey("test_key");
        return jwtAccessTokenConverter;
    }
}

2. AuthorizationServerConfig中添加配置信息

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserService userService;

    @Autowired
    @Qualifier("jwtTokenStore")
    private TokenStore tokenStore;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //为了演示方便先放在内存中, 实际开发不是如此
        clients.inMemory()
                //客户端ID
                .withClient("client")
                //密钥
                .secret(passwordEncoder.encode("112233"))
                //重定向地址
                .redirectUris("http://www.baidu.com")
                //授权范围
                .scopes("all")
                /**
                 * 授权类型   (可以支持多个模式同时使用)
                 * authorization_code 授权码模式
                 * password 密码模式
                 */
                .authorizedGrantTypes("authorization_code","password");
    }

    /**
     * 密码模式
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userService)
                    //accessToken转成JwtToken
                    .tokenStore(tokenStore)
                    .accessTokenConverter(jwtAccessTokenConverter);
    }
}

3. 使用Postman进行测试

依然使用密码模式

image-20210706083701136

在Jwt官网解码可得

image-20210706083836545

10.1 拓展JWT存储内容

新建JwtTokenEnhancer扩展内存类

JwtTokenEnhancer.java

/**
 * 拓展JWT存储内容
 */
public class JwtTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {

        Map<String, Object> map = new HashMap<>();
        map.put("enhance","enhance info");
        //将oAuth2AccessToken强转为DefaultOAuth2AccessToken类型
        ((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);

        return oAuth2AccessToken;
    }
}

可以看到参数OAuth2AccessToken是一个接口而其实现类中DefaultOAuth2AccessToken包含了一个setAdditionalInformation方法

image-20210706091950594

所需参数就是一个Map

JwtTokenStoreConfig中进行配置

@Bean
public JwtTokenEnhancer jwtTokenEnhancer(){
    return new JwtTokenEnhancer();
}

**在AuthorizationServerConfig**中配置tokenEnhancer

image-20210706092617736

添加tokenEnhancer查看该方法可以知道这是一个接口 去找到他的实现类

image-20210706092758647

所以 需要准备该实现类

注入JwtTokenEnhancer

@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;

最终代码

@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;

/**
     * 密码模式
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //设置Jwt增强内容
        TokenEnhancerChain chain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(jwtAccessTokenConverter);
        chain.setTokenEnhancers(delegates);

        endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userService)
                    //accessToken转成JwtToken
                    .tokenStore(tokenStore)
                    .accessTokenConverter(jwtAccessTokenConverter)
                    //设置Jwt增强内容
                    .tokenEnhancer(chain);
    }
}

测试

使用postman进行发送得到token 去官网进行解码查看

image-20210706093609188

10.2 解析Jwt内容

1. 添加jwt依赖

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

2. 修改UserController

@RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication, HttpServletRequest httpServletRequest){
//获取请求头中的Authorization
        String header = httpServletRequest.getHeader("Authorization");
        //截取Authorization中的token
        String token = header.substring(header.lastIndexOf("bearer") + 7);
        return Jwts.parser()
                .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))
                .parseClaimsJws(token)
                .getBody();
    }

3. 测试 使用postman

  • 首先使用之前密码模式获取token

    image-20210706095431285

  • 通过jwt令牌解析

    image-20210706095307825

    image-20210706095524673

10.3 刷新令牌

AuthorizationServerConfig中配置

配置失效时间和刷新令牌模式

image-20210706101657115

测试

使用postman获取令牌 可以看到多了一个刷新令牌

image-20210706101754237

为了测试 在开一个测试刷新令牌

填写Authorization和body 测试可得到一个新的token

image-20210706102120712

然后就可以用新令牌进行访问

11. 单点登录

​ 什么是单点登录?单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分

11.1 基于springSecurityOauth2集成SSO

image-20210706175853426

以上面的项目做为认证服务器来使用 (作为单点登录系统里的认证系统)

创建对应的springboot客户端进行测试

image-20210707084112249

  1. 在客户端导入依赖
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.onlylmf</groupId>
    <artifactId>oauth2-client01-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>oauth2-client01-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

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

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

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

        <!--commons-pool2 对象池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>

    <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>
  1. 配置

编写配置文件

server:
  port: 8081
  #防止Cookie冲突, 冲突会导致登录验证不通过
  servlet:
    session:
      cookie:
        name: OAUTH2-CLIENT-SESSIONID01


#授权服务器地址
oauth2-server-url: http://localhost:8080

#与授权服务器对应的配置
security:
  oauth2:
    client:
      client-id: client
      client-secret: 112233  #自己服务端配置的密钥
      user-authorization-uri: ${oauth2-server-url}/oauth/authorize #获取授权码
      access-token-uri: ${oauth2-server-url}/oauth/token #获取accessToken
    resource:
      jwt:
        key-uri: ${oauth2-server-url}/oauth/token_key #获取jwt令牌

登录接口

image-20210707090448206

开启sso单点登录

image-20210707090526319

  1. 在服务端配置

image-20210707090908482

修改重定向地址为客户端登录接口

配置身份认证信息

image-20210707090703468

  1. 测试访问
  • 访问

    image-20210707093616814

    则默认跳转到

    image-20210707093659165

    登录调用的是服务端之前定义的UserDetailsService时的登录逻辑

    由于在服务端设置了自动授权.autoApprove(true)

    image-20210707094105748

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值