SpringBoot 整合 oauth2(三)实现 token 认证 - 简书

标签: | 发表时间:2019-09-01 19:22 | 作者:
出处:https://www.jianshu.com

关于session和token的使用,网上争议一直很大。
总的来说争议在这里:

  1. session是空间换时间,而token是时间换空间。session占用空间,但是可以管理过期时间,token管理部了过期时间,但是不占用空间.
  2. sessionId失效问题和token内包含。
  3. session基于cookie,app请求并没有cookie 。
  4. token更加安全(每次请求都需要带上)。

第一章顺风车: SpringBoot 整合 Security(一)实现用户认证并判断返回json还是view
第二章顺风车: SpringBoot 整合 Security(二)实现验证码登录

开始正文了...

本文大概流程:

oauth2流程简介

百度百科: https://baike.baidu.com/item/OAuth2.0/6788617?fr=aladdin
详情请参考文档: https://tools.ietf.org/html/rfc6749
授权类型介绍(四种授权): https://oauth.net/2/grant-types/

我在这里只介绍常用的密码授权。

Oauth2 密码授权流程

在oauth2协议里,每一个应用都有自己的一个clientId和clientSecret(需要去认证方申请),所以一旦想通过认证,必须要有认证方下发的clientId和secret。


原理这块确实很麻烦,希望不理解的多看看参考文档。

1. pom

      <!--security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>

2. 项目架构介绍


我们需要这七个类来完成。

3. UserDetail实现认证第一步

MyUserDetailsService.java

      /**
 * Created by Fant.J.
 */
@Component
public class MyUserDetailsService implements UserDetailsService {

    @Reference(version = "2.0.0")
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String passwd = "";
        System.out.println("收到的账号"+username);
        if (CheckFormat.isEmail(username)){
             passwd = userService.selectPasswdByEmail(username);
        }else if (CheckFormat.isPhone(username)){
             passwd = userService.selectPasswdByPhone(username);
        }else {
            throw new RuntimeException("登录账号不存在");
        }
        System.out.println("查到的密码"+passwd);
        return new User(username, passwd, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
    }
}

这里重写了security认证UserDetailsService 的接口方法,添加了自定义数据库密码的查询和校验。

为什么我把它放在了controller包了呢,因为我用的dubbo,@Reference注解扫描包是controller。这注解在别的包下失效。没搞过dubbo的朋友就把它当作是调用service层就行。

4. 认证成功/失败处理器

这部分在security的 整合(一)就有,这里稍有改动。

改动一:去掉了返回view还是json的判断,统一返回json。
改动二:修改登陆成功处理器,添加oauth2 客户端的认证。

MyAuthenticationFailHandler.java

      /**
 * 自定义登录失败处理器
 * Created by Fant.J.
 */
@Component
public class MyAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        logger.info("登录失败");
            //设置状态码
            response.setStatus(500);
            response.setContentType("application/json;charset=UTF-8");
            //将 登录失败 信息打包成json格式返回
            response.getWriter().write(JSON.toJSONString(ServerResponse.createByErrorMessage(exception.getMessage())));
    }
}

MyAuthenticationSuccessHandler.java

      /**
 * Created by Fant.J.
 */
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;


    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        logger.info("登录成功");


        String header = request.getHeader("Authorization");
        if (header == null && !header.startsWith("Basic")) {
            throw new UnapprovedClientAuthenticationException("请求投中无client信息");
        }
        String[] tokens = this.extractAndDecodeHeader(header, request);

        assert tokens.length == 2;

        //获取clientId 和 clientSecret
        String clientId = tokens[0];
        String clientSecret = tokens[1];

        //获取 ClientDetails
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

        if (clientDetails == null){
            throw new UnapprovedClientAuthenticationException("clientId 不存在"+clientId);
            //判断  方言  是否一致
        }else if (!StringUtils.equals(clientDetails.getClientSecret(),clientSecret)){
            throw new UnapprovedClientAuthenticationException("clientSecret 不匹配"+clientId);
        }
        //密码授权 模式, 组建 authentication
        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP,clientId,clientDetails.getScope(),"password");

        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request,authentication);

        OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

        //判断是json 格式返回 还是 view 格式返回
        //将 authention 信息打包成json格式返回
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ServerResponse.createBySuccess(token)));
    }


    /**
     * 解码请求头
     */
    private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {
        byte[] base64Token = header.substring(6).getBytes("UTF-8");

        byte[] decoded;
        try {
            decoded = Base64.decode(base64Token);
        } catch (IllegalArgumentException var7) {
            throw new BadCredentialsException("Failed to decode basic authentication token");
        }

        String token = new String(decoded, "UTF-8");
        int delim = token.indexOf(":");
        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        } else {
            return new String[]{token.substring(0, delim), token.substring(delim + 1)};
        }
    }
}

描述:
从request的请求头中拿到Authorization信息,根据clientId获取到secret和请求头中的secret信息做对比,如果正确,组建一个新的TokenRequest类,然后根据前者和clientDetails创建OAuth2Request对象,然后根据前者和authentication创建OAuth2Authentication对象。最后通过AuthorizationServerTokenServices和前者前者创建OAuth2AccessToken对象。然后将token返回。
提示:
密码授权,我们在请求token的时候,需要一个包含clientid和clientSecret的请求头还有三个参数。

  1. 请求头:Authorization -> Basic aW50ZXJuZXRfcGx1czppbnRlcm5ldF9wbHVz 。注意是Basic 开头然后是clientid:clientScret 格式进行base64加密后的字符串。
  2. 请求参数:username和password是必须要的参数,值对应的就是账号密码,还有给可有可无的就是scope,它来声明该用户有多大的权限,默认是all。grant_type也是默认的参数,默认是password,它表示你以哪种认证模式来认证。
这是请求头解密现场

5. 配置认证/资源服务器

MyResourceServerConfig.java

      /**
 * 资源服务器
 * Created by Fant.J.
 */
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailHandler myAuthenticationFailHandler;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //表单登录 方式
        http.formLogin()
                .loginPage("/authentication/require")
                //登录需要经过的url请求
                .loginProcessingUrl("/authentication/form")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailHandler);

        http
                .authorizeRequests()
                .antMatchers("/user/*")
                .authenticated()
                .antMatchers("/oauth/token").permitAll()
                .anyRequest()
                .permitAll()
                .and()
                //关闭跨站请求防护
                .csrf().disable();
    }
}

我这里只需要认证 /user/*开头的url。 @EnableResourceServer这个注解就决定了这十个资源服务器。它决定了哪些资源需要什么样的权限。

MyAuthorizationServerConfig.java

      /**
 * 认证服务器
 * Created by Fant.J.
 */
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        super.configure(security);
    }

    /**
     * 客户端配置(给谁发令牌)
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("internet_plus")
                .secret("internet_plus")
                //有效时间 2小时
                .accessTokenValiditySeconds(72000)
                //密码授权模式和刷新令牌
                .authorizedGrantTypes({"refresh_token","password"})
                .scopes( "all");
    }

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

@EnableAuthorizationServer就代表了它是个认证服务端。

一般来讲,认证服务器是第三方提供的服务,比如你想接入qq登陆接口,那么认证服务器就是腾讯提供,然后你在本地做资源服务,但是认证和资源服务不是非要物理上的分离,只需要做到逻辑上的分离就好。

认证服务中,我们获取到ClientDetailsServiceConfigurer 并设置clientId和secret还有令牌有效期,还有支持的授权模式,还有用户权限范围。

执行

好了,启动项目。

1. 给/oauth/token 发送post请求获取token

请求头:Authorization:Basic +clientid:secret 的base64加密字符串 (认证服务器中设置的client信息)
请求参数:username password (用户登陆账号密码)

      {
    "data": {
        "refreshToken": {
            "expiration": 1528892642111,
            "value": "xxxxxx-xxxxxx-xxxxx-xxxxxxxx"
        },
        "scope": [
            "all"
        ],
        "tokenType": "bearer",
        "value": "xxxxxx-xxxxxx-xxxxx-xxxxxxxx"
    },
    "status": 200,
    "success": true
}
2. 给/oauth/token 发送post请求刷新token

请求头: 不需要
请求参数:

  1. grant_type:refresh_token
  2. refresh_token:获取token时返回的refreshToken的value
3. 访问受保护的资源,比如/user/2

类型:get请求
请求头:Authorization: bearer + tokenValue


相关 [springboot oauth2 token] 推荐:

SpringBoot 整合 oauth2(三)实现 token 认证 - 简书

- -
关于session和token的使用,网上争议一直很大. session是空间换时间,而token是时间换空间. session占用空间,但是可以管理过期时间,token管理部了过期时间,但是不占用空间.. sessionId失效问题和token内包含. session基于cookie,app请求并没有cookie.

SpringBoot的事务管理

- - ImportNew
Springboot内部提供的事务管理器是根据autoconfigure来进行决定的. 比如当使用jpa的时候,也就是pom中加入了spring-boot-starter-data-jpa这个starter之后(之前我们分析过 springboot的自动化配置原理). Springboot会构造一个JpaTransactionManager这个事务管理器.

springboot aop日志记录

- - 编程语言 - ITeye博客
一、POM增加AOP JAR包. 三、SysAspect类. 注:@annotation(cn.com.hfai.controller.system.Logweb) 一定要指定Logweb类. 四、在Controller类的方法之上加上注解 @Logweb 即可. 注:这个只是打印在控制台上,若想放到数据库中,则需要增加操作数据库的业务代码.

RSA的SecureID token数据被偷了?

- ripwu - 张志强的网络日志
博客 » 记事本 » 密码学 ». WSJ报道:RSA承认其数据被偷,4000万SecureID token需要被更新. 中国银行银行密钥用的就是RSA生产,就是下图这玩意儿,手里有这玩意儿的同学们要小心了(当然,如果你的账户里的钱没有6位数以上,也不用太担心,毕竟网银的安全性不全依赖于这个设备):.

什么是 JWT -- JSON WEB TOKEN - 简书

- -
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(. (RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景. JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密.

springboot集成shiro 实现权限控制

- - CSDN博客编程语言推荐文章
apache shiro 是一个轻量级的身份验证与授权框架,与spring security 相比较,简单易用,灵活性高,springboot本身是提供了对security的支持,毕竟是自家的东西. springboot暂时没有集成shiro,这得自己配. 本文实现从数据库读取用户信息,获取当前用户的权限或角色,通过配置文件过滤用户的角色或权限.

让SpringBoot启动更快一点

- - ImportNew
这是 2018 Spring One Platform 中的一场会议. 看完会议视频,我自己动手试了一下. 还没有观看视频的朋友推荐看一下,非常有意思. ↓我使用的是 OpenJDK 11. ❯ java --version openjdk 11.0.1 2018-10-16 OpenJDK Runtime Environment 18.9 (build 11.0.1+13) OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13, mixed mode).

springboot 整合retry(重试机制) - 简书

- -
当我们调用一个接口可能由于网络等原因造成第一次失败,再去尝试就成功了,这就是重试机制,spring支持重试机制,并且在Spring Cloud中可以与Hystaix结合使用,可以避免访问到已经不正常的实例. 写一个简单的demo,加入依赖:. @EnableRetry注解,表示启用重试机制. 定义一个简单的controller层:.

【转】oauth2开放认证协议原理及案例分析

- - 研发管理 - ITeye博客
OAuth认证协议原理分析及使用方法. ,虽然 OAuth2还没有正式发布,但是国内外的OAuth2的采用情况几乎要完全替代掉OAuth1.1了. 像淘宝、腾讯、人人网、百度开放平台就已经采用Oauth2,新浪微博也发来邮件说是要很快上马OAuth2,彻底替换掉OAuth1.1. ,最新的版本是 2011年7月25号发布的,协议变化还是很快的,所以看到国内的一些已经实现的实例,再比照官方的 oauth2,会有些出入的.

Spring security oauth2最简单入门环境搭建--二、干货

- - ITeye博客
关于OAuth2的一些简介,见我的上篇blog: http://wwwcomy.iteye.com/blog/2229889 PS:貌似内容太水直接被鹳狸猿干沉. 友情提示 学习曲线:spring+spring mvc+spring security+Oauth2基本姿势,如果前面都没看过请及时关闭本网页.