Springboot + Security + JWT + OAuth2 整合簡單案例

參照上次 Spring Security + JWT 的簡單應用

一、建立一個Springboot項目,最終的項目結構如下

二、添加pom依賴

        <!-- OAuth2 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.0.12.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.9.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

三、習慣性修改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<String, String> userMap = new HashMap<>(16);//用戶
    private static Map<String, String> userAuthMap = new HashMap<>(16);//用戶權限
    private static Map<String, String> 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<GrantedAuthority> 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<String, Object> 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<TokenEnhancer> 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請求方式,不可以直接在瀏覽器地址欄請求。

結果:

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

結果相同。

3. 把client_id和client_secret用英文冒號連接,進行base64url編碼,然後放到頭部信息Authorization中,加Basic 前綴。

得到正確的結果。

4. 我們再把訪問token中有效載荷部分解析一下,會看到我們放置在其中的用戶名、權限列表、附加信息,client_id,還有我們自己設置的過期時間

有效時間被我設置爲一天。

本次測試圓滿完成。

九、填坑記

1. 本次主要目的是測試password模式,不過在加上配置文件之後,password模式必須要AuthenticationManager的配置;

2. 在password模式下,總是出現很多次調用太深的error,我把yml中配置的用戶名和密碼去掉,在內存中放置的用戶信息,並且指定了密碼編碼器才得以解決。

十、說明

在分佈式中,oauth2只是用來提供jwt,分佈式業務服務中,是接收訪問令牌並加以解析,同樣的,需要提供相同的JWT資料,比如指紋,或者祕鑰文件。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章