Springboot + Security + JWT + OAuth2 整合简单案例

参照上次 Spring Security + JWT 的简单应用

一、建立一个Springboot项目,最终的项目结构如下

Springboot + Security + JWT + OAuth2 整合简单案例_第1张图片

二、添加pom依赖

        
        
            org.springframework.security.oauth
            spring-security-oauth2
            2.0.12.RELEASE
        
        
            org.springframework.security.oauth.boot
            spring-security-oauth2-autoconfigure
            2.1.9.RELEASE
        

        
            org.projectlombok
            lombok
            true
        

三、习惯性修改yml

server:
  port: 6008

四、建立好一个预设的文件

1. 常量表

package com.chris.oauth.consts;

/**
 * create by: Chris Chan
 * create on: 2019/9/26 8:08
 * use for: 全局常量
 */

public interface AppConstants {
    //JWT默认指纹
    String JWT_SECRET_KEY_NORMAL="JKHDWNCJKLFKKJHDKEKLLDKJKLFHJKHSGHAJKFJLNKSFLLKDLKL";
    //响应头Authorization
    String KEY_AUTHORIZATION="Authorization";
}

2. 用户信息实体

package com.chris.oauth.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * create by: Chris Chan
 * create on: 2019/9/26 8:09
 * use for:
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
    private String username;
    private String password;
}

五、编写与Security相关的几个文件

1. SecurityConfig配置

package com.chris.oauth.config;

import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

/**
 * create by: Chris Chan
 * create on: 2019/10/12 2:13
 * use for:
 */
@Configuration
@EnableWebSecurity
@EnableAuthorizationServer
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    PasswordEncoder passwordEncoder;
    @Autowired
    UserService userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().and().csrf().disable()
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated();

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userService)
                .passwordEncoder(passwordEncoder);
    }

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


在这个配置文件中放行"/oauth/token"接口URL,这是我们oauth需要用到的主要接口。这个配置文件会在oauth2的配置文件之前执行。

2. UserService

package com.chris.oauth.service;

import com.chris.oauth.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * create by: Chris Chan
 * create on: 2019/10/11 12:31
 * use for:
 */
@Service
public class UserService implements UserDetailsService {
    private static Map userMap = new HashMap<>(16);//用户
    private static Map userAuthMap = new HashMap<>(16);//用户权限
    private static Map userAdditionalMap = new HashMap<>(16);//用户附加信息 测试
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isEmpty(username)) {
            return null;
        }
        UserInfo userInfo = findUser(username);
        if (null == userInfo) {
            return null;
        }
        return new User(username, userInfo.getPassword(), getAuthorityList(username));
    }

    /**
     * 返回密码
     * 这个方法可以假设是从数据库dao层获取到用户信息
     *
     * @param username
     * @return
     */
    private UserInfo findUser(String username) {
        if (null == userMap) {
            userMap = new HashMap<>(16);
        }
        //内置几个用户
        if (userMap.size() == 0) {
            userMap.put("zhangsanfeng", passwordEncoder.encode("123123"));
            userMap.put("lisifu", passwordEncoder.encode("123123"));
            userMap.put("songzihao", passwordEncoder.encode("123123"));
        }
        String password = userMap.get(username);
        if (StringUtils.isEmpty(password)) {
            return null;
        }
        return new UserInfo(username, password);
    }

    /**
     * 获取用户权限
     * 这个方法也可以在数据库中查询
     *
     * @param username
     * @return
     */
    public static String[] getAuthorities(String username) {

        if (null == userAuthMap) {
            userAuthMap = new HashMap<>(16);
        }
        //内置几个用户权限
        if (userAuthMap.size() == 0) {
            userAuthMap.put("zhangsanfeng", "ROLE_ADMIN,ROLE_USER");
            userAuthMap.put("lisifu", "ROLE_ADMIN,ROLE_USER");
            userAuthMap.put("songzihao", "ROLE_SYS,ROLE_ADMIN,ROLE_USER");
        }
        return userAuthMap.get(username).split(",");
    }

    /**
     * 获取用户权限集合
     *
     * @param username
     * @return
     */
    public static List getAuthorityList(String username) {
        return AuthorityUtils.createAuthorityList(getAuthorities(username));
    }

    /**
     * 获取用户的附加信息 测试使用 用以扩展JWT
     *
     * @param username
     * @return
     */
    public static String getUserAdditional(String username) {

        if (null == userAdditionalMap) {
            userAdditionalMap = new HashMap<>(16);
        }
        //内置几个用户权限
        if (userAdditionalMap.size() == 0) {
            userAdditionalMap.put("zhangsanfeng", "worker");
            userAdditionalMap.put("lisifu", "student");
            userAdditionalMap.put("songzihao", "teacher");
        }
        return userAdditionalMap.get(username);
    }
}

增加了一个用户附加信息,这些在实际业务中都是可以从数据库中去查询的,测试目的是扩展jwt的信息。

六、有关的Bean的管理,由于Bean比较多,所以我们把他们收集起来管理。

package com.chris.oauth.config;

import com.chris.oauth.consts.AppConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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;

/**
 * create by: Chris Chan
 * create on: 2019/10/12 12:35
 * use for:
 */
@Configuration
public class BeanBox {
    /**
     * 密码编码器
     * 这个实例中security和oauth都是用一个密码编码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * jwt生成处理
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * token转换器
     * @return
     */
    @Bean
    protected JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray());
//        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
        converter.setSigningKey(AppConstants.JWT_SECRET_KEY_NORMAL);//设置JWT指纹
        return converter;
    }

}

七、JWT扩展器

package com.chris.oauth.config;

import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * create by: Chris Chan
 * create on: 2019/10/12 12:39
 * use for: JWT扩展处理
 */
@Component
@Primary
public class JWTTokenEnhancer implements TokenEnhancer {
    @Autowired
    UserService userService;

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        String username = user.getUsername();
        //添加附加信息
        Map map = new HashMap<>(16);
        map.put("additional", userService.getUserAdditional(username));
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
        //设置过期时间
        ((DefaultOAuth2AccessToken) accessToken).setExpiration(new Date(LocalDateTime.now().plusDays(1).toInstant(ZoneOffset.of("+8")).toEpochMilli()));
        return accessToken;
    }
}

默认的JwtTokenStore产生的jwt只包含用户名和权限列表,如果我们需要为登录用户在jwt中添加其他的必要信息,就要编写这个。

八、与OAuth2相关的两个配置文件

package com.chris.oauth.config;

import com.chris.oauth.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.token.TokenEnhancer;
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 java.util.ArrayList;
import java.util.List;

/**
 * create by: Chris Chan
 * create on: 2019/10/12 2:15
 * use for:
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
    @Autowired
    PasswordEncoder passwordEncoder;
    @Autowired
    UserService userService;
    @Autowired
    TokenStore tokenStore;
    @Autowired
    TokenEnhancer tokenEnhancer;
    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private AuthenticationManager authenticationManager;

    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")
//                .checkTokenAccess("isAuthenticated()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient("clientId")
                .secret(passwordEncoder.encode("clientSecret"))
                .redirectUris("http://localhost:6008/callback")
                .authorizedGrantTypes("authorization_code", "password", "implicit", "client_credentials", "refresh_token")
                .resourceIds("resId")
                .scopes("scope1", "scope2");
    }

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

        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List enhancerList = new ArrayList<>();
        enhancerList.add(tokenEnhancer);
        enhancerList.add(jwtAccessTokenConverter);
        enhancerChain.setTokenEnhancers(enhancerList);

        endpoints.tokenEnhancer(enhancerChain)
                .accessTokenConverter(jwtAccessTokenConverter);

    }

}

 租后一个方法就是配置jwt和扩展工具等等。

package com.chris.oauth.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

/**
 * create by: Chris Chan
 * create on: 2019/9/27 16:51
 * use for:
 */
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .requestMatchers().antMatchers("/**")
                .and()
                .authorizeRequests()
                .antMatchers("/**").authenticated();
    }
}

九、测试

程序运行起来,我们用PostMan来测试

1. 把所有参数全部添加到url中:

localhost:6008/oauth/token?grant_type=password&username=songzihao&password=123123&client_id=clientId&client_secret=clientSecret

因为/oauth/token是post请求方式,不可以直接在浏览器地址栏请求。

结果:

Springboot + Security + JWT + OAuth2 整合简单案例_第2张图片

2. client_id和client_secret不出现在url中,而是用basic auth的方式提交

Springboot + Security + JWT + OAuth2 整合简单案例_第3张图片

Springboot + Security + JWT + OAuth2 整合简单案例_第4张图片

结果相同。

3. 把client_id和client_secret用英文冒号连接,进行base64url编码,然后放到头部信息Authorization中,加Basic 前缀。

Springboot + Security + JWT + OAuth2 整合简单案例_第5张图片

Springboot + Security + JWT + OAuth2 整合简单案例_第6张图片

得到正确的结果。

4. 我们再把访问token中有效载荷部分解析一下,会看到我们放置在其中的用户名、权限列表、附加信息,client_id,还有我们自己设置的过期时间

Springboot + Security + JWT + OAuth2 整合简单案例_第7张图片

有效时间被我设置为一天。

Springboot + Security + JWT + OAuth2 整合简单案例_第8张图片

本次测试圆满完成。

九、填坑记

1. 本次主要目的是测试password模式,不过在加上配置文件之后,password模式必须要AuthenticationManager的配置;

2. 在password模式下,总是出现很多次调用太深的error,我把yml中配置的用户名和密码去掉,在内存中放置的用户信息,并且指定了密码编码器才得以解决。

十、说明

在分布式中,oauth2只是用来提供jwt,分布式业务服务中,是接收访问令牌并加以解析,同样的,需要提供相同的JWT资料,比如指纹,或者秘钥文件。

 

你可能感兴趣的:(Java,架构,Spring,Boot)