oatuth2.0 + JWT 案例

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 上一节,我们讲述了Oauth2.0 配合security的使用,配合redis去存token,或者配合Jwt去存token。这篇文章,我们主要来系统的串起来讲一下,从宏观的层面来讲述一下单点登录,并且来实现一个demo。

上一节,我们讲述了Oauth2.0 配合security的使用,配合redis去存token,或者配合Jwt去存token。这篇文章,我们主要来系统的串起来讲一下,从宏观的层面来讲述一下单点登录,并且来实现一个demo。

首先,我们来借助一个真实的案例来切入:

相信大家都登录过码云吧:(https: //gitee.com/)

image-20210225134304440

在登录选项里我们选择使用三方账号进行登录(QQ)

然后他就会跳转到下面这个地址:

https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101284669&redirect_uri=https%3A%2F%2Fgitee.com%2Fauth%2Fqq_connect%2Fcallback&response_type=code&state=89f4057a9c58e68c18eada439aa52d9400b71e8efdf7201e

image-20210225150507938

这个地址有没有很熟悉?

路径里面的几个参数是不是很熟悉?client_id,redirect_uri,response_type

上面一节课我们了解了这几个概念: 认证服务器,客户端。这里我们进行对号入座:

认证服务器:就是上面的跳转地址(QQ的认证服务器)

客户端:码云

上面的认证就是授权码模式进行认证,登陆QQ后,QQ的认证服务器通过你的认证,返回授权码和回调地址(redirect_uri):码云拿到授权码进行令牌(token)的获取

image-20210225150815009

通过检查调用的接口情况,发现码云根据上面的callback接口获取到code,然后在接口内部去调用QQ的获取token的接口(默认的是 /ooauth/token)的接口。这样的话进行就能够拿到token了,拿到token后可以进行重定向到login页面,请求QQ那边的获取用户基本信息的接口(资源)来获取QQ的基本信息(昵称,用户名)来回填到码云的登录表单中。

这样我们就基本了解什么是单点登录了。

下面我们写一个demo:

1. 新建一个服务(就好比QQ的认证服务)

依赖:

<?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.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.starzyn</groupId>
    <artifactId>sso</artifactId>
    <version>0.0.1</version>
    <name>sso</name>
    <description>sso</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <version>10.10.1</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-nacos-discovery</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.5.7</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

配置文件:

server:
  port: 9989
spring:
  datasource:
    url: jdbc:mysql://your-server:23306/sso?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: tiger
    driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: http://39.102.83.104:8848
feign:
  okhttp:
    enabled: true

用户实体类:

package com.starzyn.sso.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

/**
 * <p>
 * 后台用户表
 * </p>
 *
 * @author starzyn
 * @since 2021-02-23
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("ums_admin")
@ApiModel(value="UmsAdmin对象", description="后台用户表")
public class UmsAdmin implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    private String username;

    private String password;

    @ApiModelProperty(value = "头像")
    private String icon;

    @ApiModelProperty(value = "邮箱")
    private String email;

    @ApiModelProperty(value = "昵称")
    private String nickName;

    @ApiModelProperty(value = "备注信息")
    private String note;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime createTime;

    @ApiModelProperty(value = "最后登录时间")
    private LocalDateTime loginTime;

    @ApiModelProperty(value = "帐号启用状态:0->禁用;1->启用")
    private Integer status;


}

security认证使用的用户类:

package com.starzyn.sso.entity;

import cn.hutool.crypto.digest.BCrypt;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.*;

import static cn.hutool.crypto.digest.BCrypt.hashpw;

/**
 * @author starzyn
 * @className:SecurityUser
 * @date : 2021/2/23
 * @description:
 */
public class SecurityUser implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private Collection<SimpleGrantedAuthority> authorities;
    private boolean enabled;

    public SecurityUser(){}

    public SecurityUser(UmsAdmin admin){
        this.id = admin.getId();
        this.username = admin.getUsername();
        this.password = admin.getPassword();
        this.authorities = new HashSet<>();
        Arrays.stream(admin.getNote().split(",")).forEach(role -> this.authorities.add(new SimpleGrantedAuthority(role)));
        this.enabled = Objects.equals(1, admin.getStatus()) ? true : false;
    }

//    public static void main(String[] args) {
//        System.out.println(BCrypt.hashpw("123456"));
//
//    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.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 enabled;
    }
}

用户信息的业务层:

package com.starzyn.sso.api.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.starzyn.sso.entity.SecurityUser;
import com.starzyn.sso.entity.UmsAdmin;
import com.starzyn.sso.mapper.UmsAdminMapper;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * @author starzyn
 * @className:UserService
 * @date : 2021/2/23
 * @description:
 */
@Service
public class UserServiceImpl implements UserDetailsService {
    @Autowired
    private UmsAdminMapper umsAdminMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {
        List<UmsAdmin> users = umsAdminMapper.selectList(new QueryWrapper<UmsAdmin>().eq("username", username));
        if (CollectionUtils.isEmpty(users)) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        return new SecurityUser(users.get(0));
    }
}

认证服务配置:

package com.starzyn.sso.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;

/**
 * @author starzyn
 * @className:AuthServer
 * @date : 2021/2/23
 * @description:
 */
@Configuration
@EnableAuthorizationServer
public class AuthServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetailsService userServiceImpl;

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

    @Autowired
    private AccessTokenConverter jwtAccessTokenConverter;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("portal")
                .authorizedGrantTypes("authorization_code", "password", "refresh_token")
                .redirectUris("http://localhost:8080/callback")
                .secret(passwordEncoder.encode("admin"))
                .scopes("all");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userServiceImpl)
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();//这个地方需要注意一下,如果不进行配置,就会抱401的问题,通过跟源码发现,如果不配置,他就会使用你前面进行认证的那个 Authentication对象,这样的话就会需要加头,进行认证,用postman进行请求就是可以的,但是使用restTemplate 请求就会抱401
    }
}

security的配置:

package com.starzyn.sso.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

/**
 * @author starzyn
 * @className:SpringSecurityConfig
 * @date : 2021/2/23
 * @description:
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter  {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/oauth/**", "/rsa/publicKey", "/v2/api-docs", "/callback")
            .permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .permitAll();
    }
}

Jwt的配置

package com.starzyn.sso.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class JwtTokenStoreConfig {

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

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("starzyn");
        return jwtAccessTokenConverter;
    }

}

2. 新建一个客户端(就好比码云)

新起一个服务(端口8080)

测试接口:

package com.starzyn.client1.api.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UserController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("callback")
    public String getToken(@RequestParam(required = false) String code) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("code", code);
        params.add("client_id", "portal");
        params.add("client_secret", "admin");
        params.add("redirect_uri", "http://localhost:8080/callback");
//        HttpHeaders httpHeaders = new HttpHeaders();
//        httpHeaders.add("Content-type", "application/x-www-form-urlencoded");
//        httpHeaders.add("Authorization", "Basic " + Base64Utils.encodeToString("portal:admin".getBytes()));
//        HttpEntity<MultiValueMap> entity = new HttpEntity<>(httpHeaders, params);
        ResponseEntity<String> resp = restTemplate.postForEntity("http://localhost:9989/oauth/token", params, String.class);
        return resp.getBody();
    }

}

3. 测试

启动两个服务,访问 http: //localhost:9989/oauth/authorize?response_type=code&client_id=portal&redirect_uri=http: //localhost:8080/callback&scope=all

image-20210225164541854

这个页面就是类比跳转到QQ的认证服务,进行QQ的登陆

输入用户名和密码之后

image-20210225164644581

这就好比QQ对码云的授权

点击Authorize之后跳转到 http: //localhost:8080/callback?code=1MWurK

image-20210225164759282

这就是码云拿到了QQ认证服务给的token

整个流程走下来,token拿到了,其他的就是去资源服务器上去访问资源了(请求QQ的获取用户的接口)

整个流程走下来,就是一个根据授权码模式来进行单点登录的方式了。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
安全 Java 数据库
Spring Security详细讲解(JWT+SpringSecurity登入案例)
通过本篇博文,你可以详细了解Spring Security的相关概念与原理,并且掌握Spring Security的认证与授权,通过博文中的登入案例可以让自己定制去Spring Security认证授权方案。
Spring Security详细讲解(JWT+SpringSecurity登入案例)
|
存储 SQL 算法
【案例实战】分布式应用下登录检验解决方案(JWT)
【案例实战】分布式应用下登录检验解决方案(JWT)
【案例实战】分布式应用下登录检验解决方案(JWT)
|
存储 JSON JavaScript
【项目实战】一、Spring boot整合JWT、Vue案例展示用户鉴权(下)
【项目实战】一、Spring boot整合JWT、Vue案例展示用户鉴权(下)
160 0
|
JavaScript 前端开发 Java
【项目实战】一、Spring boot整合JWT、Vue案例展示用户鉴权(中)
【项目实战】一、Spring boot整合JWT、Vue案例展示用户鉴权(中)
174 0
|
JavaScript Java Nacos
【项目实战】一、Spring boot整合JWT、Vue案例展示用户鉴权(上)
【项目实战】一、Spring boot整合JWT、Vue案例展示用户鉴权(上)
198 0
jira学习案例26-用useHttp管理jwt和登录状态
jira学习案例26-用useHttp管理jwt和登录状态
69 0
jira学习案例26-用useHttp管理jwt和登录状态
jira学习案例22-jwt原理-auth-provider
jira学习案例22-jwt原理-auth-provider
64 0
jira学习案例22-jwt原理-auth-provider
|
2月前
|
SQL Java 测试技术
在Spring boot中 使用JWT和过滤器实现登录认证
在Spring boot中 使用JWT和过滤器实现登录认证
178 0
|
3月前
|
JSON 安全 Java
使用Spring Boot和JWT实现用户认证
使用Spring Boot和JWT实现用户认证
|
11天前
|
JSON NoSQL Java
springBoot:jwt&redis&文件操作&常见请求错误代码&参数注解 (九)
该文档涵盖JWT(JSON Web Token)的组成、依赖、工具类创建及拦截器配置,并介绍了Redis的依赖配置与文件操作相关功能,包括文件上传、下载、删除及批量删除的方法。同时,文档还列举了常见的HTTP请求错误代码及其含义,并详细解释了@RequestParam与@PathVariable等参数注解的区别与用法。

热门文章

最新文章