spring cloud security oauth2 + jwt

什么是OAuth2?

OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在限定时间、限定范围访问指定资源。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。获取令牌的方式主要有四种,分别是授权码模式,简单模式,密码模式和客户端模式,如何获取token不在本篇文章的讨论范围,我们这里假定客户端已经通过某种方式获取到了access_token,想了解具体的oauth2授权步骤可以移步阮一峰老师的理解OAuth 2.0,里面有非常详细的说明。
这里要先明确几个OAuth2中的几个重要概念:
resource owner: 拥有被访问资源的用户
user-agent: 一般来说就是浏览器
client: 第三方应用
Authorization server: 认证服务器,用来进行用户认证并颁发token
Resource server:资源服务器,拥有被访问资源的服务器,需要通过token来确定是否有权限访问
明确概念后,就可以看OAuth2的协议握手流程:
在这里插入图片描述
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。

什么是Spring Security?

Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一系列的filter chain来进行拦截过滤,以下是ss中默认的内置过滤器列表,当然你也可以通过custom-filter来自定义扩展filter chain列表:
在这里插入图片描述这里面最核心的就是FILTER_SECURITY_INTERCEPTOR,通过FilterInvocationSecurityMetadataSource来进行资源权限的匹配,AccessDecisionManager来执行访问策略

认证与授权(Authentication and Authorization)

一般意义来说的应用访问安全性,都是围绕认证(Authentication)和授权(Authorization)这两个核心概念来展开的。即首先需要确定用户身份,在确定这个用户是否有访问指定资源的权限。认证这块的解决方案很多,主流的有CAS、SAML2、OAUTH2等(不巧这几个都用过-_-),我们常说的单点登录方案(SSO)说的就是这块,授权的话主流的就是spring security和shiro。shiro我没用过,据说是比较轻量级,相比较而言spring security确实架构比较复杂。

JWT介绍

终于来到了著名的JWT部分了,JWT全称为Json Web Token,最近随着微服务架构的流行而越来越火,号称新一代的认证技术。今天我们就来看一下,jwt的本质到底是什么。
我们先来看一下OAuth2的token技术有没有什么痛点,相信从之前的介绍中你也发现了,token技术最大的问题是不携带用户信息,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低的,且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求,这在分布式架构下是很要命的。
JWT就是在这样的背景下诞生的,从本质上来说,jwt就是一种特殊格式的token。普通的oauth2颁发的就是一串随机hash字符串,本身无意义,而jwt格式的token是有特定含义的,分为三部分:

  1. 头部Header
  2. 载荷Payload
  3. 签名Signature
    这三部分均用base64进行编码,当中用.进行分隔,一个典型的jwt格式的token类似xxxxx.yyyyy.zzzzz。关于jwt格式的更多具体说明,不是本文讨论的重点,大家可以直接去官网查看官方文档,这里不过多赘述。
    相信看到签名大家都很熟悉了,没错,jwt其实并不是什么高深莫测的技术,相反非常简单。认证服务器通过对称或非对称的加密方式利用payload生成signature,并在header中申明签名方式,仅此而已。通过这种本质上极其传统的方式,jwt可以实现分布式的token验证功能,即资源服务器通过事先维护好的对称或者非对称密钥(非对称的话就是认证服务器提供的公钥),直接在本地验证token,这种去中心化的验证机制无疑很对现在分布式架构的胃口。jwt相对于传统的token来说,解决以下两个痛点:
    通过验证签名,token的验证可以直接在本地完成,不需要连接认证服务器
    在payload中可以定义用户相关信息,这样就轻松实现了token和用户信息的绑定
    在上面的那个资源服务器和认证服务器分离的例子中,如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。

授权模式

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
本文重点讲解接口对接中常使用的密码模式

项目代码部分

oauth微服务添加jar包:

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

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

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

项目最重要的是以下几个类:
在这里插入图片描述
TokenConfig Token配置类 代码如下:

package com.hngtsd.zxtk.oauth.config;

import com.hngtsd.zxtk.common.utlis.CommonVo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCrypt;
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;

/*
 * @author Mr.wangfeng
 * @date 2020/8/5 16:54
 * @param token配置类
 * @return
 */
@Configuration
public class TokenConfig {
    
    public static final String SIGNING_KEY ="oauth";
    /**
     * accessTokenConverter
     * 使用JWT令牌
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
        // accessTokenConverter.setSigningKey(SIGNING_KEY);  //对称密钥  资源服务访问需要验证
        accessTokenConverter.setSigningKey(CommonVo.SIGNING_KEY);  //对称密钥  资源服务访问需要验证
        return accessTokenConverter;
    }

    /**
     * 令牌储存策略
     */
    @Bean
    public TokenStore tokenStore() {
        //JWT储存
        return new JwtTokenStore(accessTokenConverter());
    }

}

WebSecurityConfig前端配置类 :

package com.hngtsd.zxtk.oauth.config;


import com.hngtsd.zxtk.oauth.config.sms.PhoneAndVerificationCodeAuthenticationProvider;
import com.hngtsd.zxtk.oauth.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.client.RestTemplate;

/*
 * @author Mr.wangfeng
 * @date 2020/8/6 15:33
 * @param 前端配置类
 * @return
 */
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * authenticationManager
     * 认证服务
     *
     * @return
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Autowired
    private MyUserDetailsService userDetailsService;
    /**
     * passwordEncoder
     * 密码编码器
     *
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 安全拦截机制
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置电话号码和验证码认证
      http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin();
        http;

       

}
}

AuthorizationServer认证配置类代码:

package com.hngtsd.zxtk.oauth.config;

import com.hngtsd.zxtk.oauth.config.sms.PhoneAndVerificationCodeTokenGranter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
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.redis.RedisTokenStore;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


/*
 * @author Mr.wangfeng
 * @date 2020/8/5 16:53
 * @param 认证授权类
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Autowired
    private AuthorizationCodeServices authorizationCodeService;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private TokenStore tokenStore;

    /**
     * authorizationCodeService
     * 授权码服务
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
        // 基于数据库
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    /**
     * clientDetailsService
     * 客户端的详情服务
     * @return
     */
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        //客户端数据存储到数据库
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService)clientDetailsService).setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }
    /**
     * <p>注意,自定义TokenServices的时候,需要设置@Primary,否则报错,</p>
     * 令牌管理服务
     * @return
     */
    @Primary
    @Bean
    public AuthorizationServerTokenServices tokenServices(){
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        //客户端的详情服务
        tokenServices.setClientDetailsService(clientDetailsService);
        // 是否刷新令牌
        tokenServices.setSupportRefreshToken(true);
        // 存储的方式
        tokenServices.setTokenStore(tokenStore);
        //令牌增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        tokenServices.setTokenEnhancer(tokenEnhancerChain);
        // token有效期自定义设置,默认12小时
        tokenServices.setAccessTokenValiditySeconds(60 * 60 * 12);
        //刷新令牌过期时间,默认24小时
        tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24);
        return tokenServices;
    }


    /**
     * 客户端配置
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }

    /**
     *   配置令牌访问端点
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints
                .authenticationManager(authenticationManager)           //密码模式需要
                .authorizationCodeServices(authorizationCodeService)    //授权码服务
                .tokenServices(tokenServices())                         //令牌服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);


    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()");              //oauth/token_key公开
        security .checkTokenAccess("permitAll()");           //oauth/check_token公开  校验token的合法性
        security.allowFormAuthenticationForClients();        // 允许表单提交 (申请令牌)
    }

}

MyUserDetailsService实现UserDetailsService接口 :

package com.hngtsd.zxtk.oauth.service;

import com.alibaba.fastjson.JSON;
import com.hngtsd.zxtk.common.model.TbPopedom;
import com.hngtsd.zxtk.common.model.TbUser;
import com.hngtsd.zxtk.oauth.mapper.TbUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class MyUserDetailsService  implements UserDetailsService {

    @Autowired
    TbUserMapper userMapper;
    /**
     * 
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        TbUser user = userMapper.selectByUsername(username);
        if (user == null) {
            return null;
        }
        if (user.getStatus() == 1) {
            return null;
        }
        List<TbPopedom> tbPopedoms = userMapper.selectTbPopedomByUid(user.getId());
        List<String> popedoms = new ArrayList<String>();
        if (tbPopedoms !=null && tbPopedoms.size()>0 ){
            for (TbPopedom popedom : tbPopedoms) {
                if (popedom.getCode() != null) {
                    popedoms.add(popedom.getCode());
                }
            }
        }
        String[] popedomsArray = new String[popedoms.size()];
        String[] array = popedoms.toArray(popedomsArray);
        //将用户信息转成JSON
        String principal = JSON.toJSONString(user);
        UserDetails userDetails = User.withUsername(principal).password(user.getPassword()).authorities(array).build();
        return userDetails;
    }
}

上面认证的数据我都是动态从数据库获取的数据,后面我会将数据库表给大家。

测试

这里面的参数一个都不能少,对应的值你可以自己设置,这里我数据库以及配好了放在配置文件中。

#token存储到redis的过期时间 (设置为一天)
tokenValiditySeconds=43200
clientId=WebApp
clientSecret=secret
cookieDomain=localhost
cookieMaxAge= -1

参数配置好了我们就可以打开postman进行测试了
这里我只使用了password测试
在这里插入图片描述
这里我填了很多的权限,所以token比较长,本项目我是使用redis将token进行存储的。
在这里插入图片描述
Spring cloud security oauth2 +jwt 使用手机验证码登陆的教程后续再给大家发,项目中已经更新了
项目地址:https://gitee.com/wf109809/security-oauth2-jwt.git

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值