springboot2整合oauth2

1.背景

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

完整项目demo:https://gitee.com/zkane/springboot2-oauth2.git

2.spring security

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

  • spring security架构图
    输入图片说明
    • 认证过程
      输入图片说明

3.OAuth2

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:8090/api/oauth/token
  • 在请求的headers中新增一个header:key=Authorization,value=Basic YmljaWNsaWVudDpiaWNpc2VjcmV0(YmljaWNsaWVudDpiaWNpc2VjcmV0为64编码,格式:cliendId:secret)
  • 在form-data中传递参数:username(用户账号)、password(用户密码)、grant_type(固定值:password)、scope(作用域)
    输入图片说明

4.5.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.6.通过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: [email protected]
 * @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.7.访问资源服务器的方式

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);
            }
        }) ;
    }
}

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": "账号未注册,请联系管理员"
}

猜你喜欢

转载自blog.csdn.net/qq_37170583/article/details/80704660