Oauth2.0 + JWT 实现单点登录

上一节,我们讲述了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的获取用户的接口)

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值