springboot2整合oauth2

1.背景

项目由springboot1.5.X升级到springboot2.0.0后,导致各组件API以及依赖包发生了变化。

完整项目demo:码云

2.spring security

Spring Security 从入门到进阶系列教程网址:http://www.spring4all.com/article/428

  • spring security架构图
    在这里插入图片描述
  • 认证过程
    [外链图片转存失败(img-Z2J4Yur9-1567653136510)(https://gitee.com/uploads/images/2018/0419/105052_53ea1406_1305332.png "屏幕截图.png")]

3.OAuth2

  • OAuth2基础概念网址:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
  • OAuth2分为3个部分:1认证服务器2资源服务器3第三方应用
  • OAuth2有4种授权模式:1授权码模式2简化模式3密码模式4客户端模式

4.使用springboot2+oauth2注意事项

  • 项目搭建参考网址:

https://blog.csdn.net/qq_19671173/article/details/79748422

http://wiselyman.iteye.com/blog/2411813

4.1.在pom.xml文件中导入依赖包发生变化

        <!-- springboot2.0已经将oauth2.0与security整合在一起 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- 由于一些注解和API从spring security5.0中移除,所以需要导入下面的依赖包  -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>
        <!-- redis相关依赖包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

4.2.直接使用RedisTokenStore存储token会出现NoSuchMethodError RedisConnection.set([B[B)V错误

解决方案:自己编写一个MyRedisTokenStore,复制RedisTokenStore类中代码,并将代码中conn.set(accessKey, serializedAccessToken)修改为conn.stringCommands().set(accessKey, serializedAccessToken);

4.3.前后端分离时,存在跨域问题

解决方案:

  • 方案一在后端注册corsFilter
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 1
        corsConfiguration.addAllowedOrigin("*");
        // 2
        corsConfiguration.addAllowedHeader("*");
        // 3
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // step 4
        source.registerCorsConfiguration("/**", buildConfig());
        FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}
  • 方案二,在启动类添加bean到IOC容器中
@SpringBootApplication
public class ZuulServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulServerApplication.class, args);
    }

    /**
     * 解决前后端分离跨域问题
     *
     * @return
     */
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

}

4.4.前后端分离,登录页面放在前端时登录的问题

解决方案:授权模式使用password的方式

  • 使用post请求访问http://localhost:20000/auth/oauth/token
  • 在请求的headers中新增一个header:key=Authorization,value=Basic Y2xpZW50OnNlY3JldA==

(Y2xpZW50OnNlY3JldA==为64编码,格式:client:secret)

  • 在form-data中传递参数:username(用户账号)、password(用户密码)、grant_type(固定值:password)、scope(作用域)
    这里写图片描述

4.5.访问资源服务器的方式

  • 当通过在认证服务器获取到token后,有三种方式访问资源服务器
  1. 在Headers中携带:key=Authorization,value=bearer 797c4200-8b10-4a2b-8764-33397749a8f7
  2. 拼接在URL中:http://localhost:8088/user?access_token=797c4200-8b10-4a2b-8764-33397749a8f7
  3. 在form表单中:name=access_token,value=797c4200-8b10-4a2b-8764-33397749a8f7

4.6.spring security密码配置问题

  • secret密码配置从 Spring Security 5.0开始必须以 {加密方式}+加密后的密码 这种格式填写
  • 当前版本5新增支持加密方式:

bcrypt - BCryptPasswordEncoder (Also used for encoding)

ldap - LdapShaPasswordEncoder

MD4 - Md4PasswordEncoder

MD5 - new MessageDigestPasswordEncoder(“MD5”)

noop - NoOpPasswordEncoder

pbkdf2 - Pbkdf2PasswordEncoder

scrypt - SCryptPasswordEncoder

SHA-1 - new MessageDigestPasswordEncoder(“SHA-1”)

SHA-256 - new MessageDigestPasswordEncoder(“SHA-256”)

sha256 - StandardPasswordEncoder

4.7.通过spring security的角色限制访问受保护的接口

  • 在配置类或启动类上添加注解 @EnableGlobalMethodSecurity(securedEnabled = true)
@EnableOAuth2Sso
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/index")
                .permitAll()
                .anyRequest()
                .authenticated();
    }
}
  • 在controller的类或方法上添加注解 @Secured(“ROLE_ADMIN”)
package com.bici.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * @author: keluosi@bicitech.cn
 * @date: 2018/4/17
 */
@RestController
@RequestMapping("/client")
@Secured("ROLE_ADMIN")
public class ClientController {

    @GetMapping("/user")
    public Authentication getUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication;
    }

    @GetMapping("/index")
    @Secured("ROLE_USER")
    public String index() {
        return "index";
    }
}

4.8.使用自定义的加密方式校验数据库中保存的加密后的密文

  • 在@EnableWebSecurity注解的方法中编写代码
import com.bici.encrypt.EncryptUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Qualifier("userDetailsService")
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected UserDetailsService userDetailsService() {
        // 自定义用户信息类
        return this.userDetailsService;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder(){
            @Override
            public String encode(CharSequence charSequence) {
                // 加密
                return EncryptUtil.hashPasswordAddingSalt(charSequence.toString());
            }
            @Override
            public boolean matches(CharSequence charSequence, String s) {
                // 密码校验
                return EncryptUtil.isValidPassword(charSequence.toString(), s);
            }
        }) ;
    }
}

4.9.通过配置返回通知获取token

可以在这个地方将token和username存入到缓存中,然后如果需要强制某个用户下线时,通过username从缓存中找到token,调用ConsumerTokenServicesrevokeToken(token)方法。

package com.zkane.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;

/**
 * @author: 594781919@qq.com
 * @review:
 * @date: 2018/8/24
 */
@Aspect
@Component
public class TokenAspect {

    @Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
    private void token() {
    }

    @AfterReturning(returning = "obj", pointcut = "token()")
    public void doAfterReturning(ResponseEntity<OAuth2AccessToken> obj) throws Exception {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 打印用户名
        System.out.println(request.getParameter("username"));
        // 打印token
        System.out.println(obj.getBody().getValue());
    }
}

5.不足或后续改进

5.1.客户端信息保存到数据中

  • 创建sql语句
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(256) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO oauth_client_details (
	client_id,
	resource_ids,
	client_secret,
	scope,
	authorized_grant_types,
	web_server_redirect_uri,
	authorities,
	access_token_validity,
	refresh_token_validity,
	additional_information,
	autoapprove
)
VALUES
	(
		'client',
		NULL,
		'{noop}secret',
		'all',
		'password,authorization_code,refresh_token,implicit,client_credentials',
		NULL,
		NULL,
		NULL,
		NULL,
		NULL,
		'true'
	);
  • 以jdbc方式配置客户端信息
@Configuration
@EnableAuthorizationServer
public class ServerConfig extends AuthorizationServerConfigurerAdapter {
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }
}

5.2.前后端分离,导致单点登录问题

  • 目前通过网上下载的demo,还没有查找到第三方应用前后端分离,怎么去登录的问题。
  • 第三方应用一般都是通过请求转发页面时,到认证中心去登录。也就是前后端未分离的情况,使用注解@EnableOAuth2Sso
  • 建议解决方案:通过前端进行跳转到唯一的登录页面,登录成功后再返回到原来系统并带上token

5.3.密码错误时,返回前端的json未实现自定义返回内容

{
    "error": "invalid_grant",
    "error_description": "Bad credentials"
}

update 2018-04-28

自定义返回前端的登录错误信息

  • 将spring-security-core\5.0.3.RELEASE\org\springframework\security\messages_zh_CN.properties拷贝到resources目录下
  • 在启动类中编写代码
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        return messageSource;
    }
}
  • 显示效果,error_description是在messages_zh_CN.properties中自己根据需要编写的
{
    "error": "invalid_grant",
    "error_description": "账号未注册,请联系管理员"
}
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值