1. 程式人生 > >spring boot security oauth2 jwt 服務端實現

spring boot security oauth2 jwt 服務端實現

最近在寫提供給第三方的登入和獲取資源,老大說要使用oauth2 協議,因此最近在啃這一塊。一下是個人的配置。

首先要理解什麼是OAUTH2協議。

以下是阮一峰大大的文章:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html。

其實在看過這篇文章之後我還是有點雲裡霧裡的,雖然明白了裡面的執行機制,但是讓自己去寫,還是有點困難,因此在一直繼續找。

以下是參考過的文章:

http://www.leftso.com/blog/139.html

https://github.com/leftso/demo-spring-boot-security-oauth2

http://conkeyn.iteye.com/blog/2296406

http://baijiahao.baidu.com/s?id=1573372009481203

http://jinnianshilongnian.iteye.com/blog/2038646

下面直接上程式碼:

首先是POM.xml檔案依賴

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

        <!-- OAuth2.0 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>
        
        <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-jwt</artifactId>
          </dependency>

再然後就是三個配置檔案

繼承WebSecurityConfigurerAdapter 類的

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
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.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 com.zhibo.xmt.api.userinfo.service.IUserService;
import com.zhibo.xmt.common.vo.AuthUserVo;


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    // 查詢使用者使用
    @Autowired
    private IUserService userService;//這個service要自己去實現

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        // auth.inMemoryAuthentication()
        // .withUser("user").password("password").roles("USER")
        // .and()
        // .withUser("app_client").password("nopass").roles("USER")
        // .and()
        // .withUser("admin").password("password").roles("ADMIN");
        //配置使用者來源於資料庫
        /**
         * 通過AuthenticationManagerBuilder的userDetailsService方法,設定進去就OK了。
         * passwordEncoder方法設定的是使用者密碼的加密方式,這裡設定的是MD5加密,
         * 所以使用者從前端登入時傳過來的密碼,在使用Security驗證時會自動使用MD5加密。
         */
        auth.userDetailsService(userDetailsService()).passwordEncoder(new Md5PasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated().and()
                .httpBasic().and().csrf().disable();
        
    
        
        
        
//        http.authorizeRequests()
//        .antMatchers("/wap/**", "/api/**","/admin/**").permitAll()
//        //其他地址的訪問均需驗證許可權
//        .anyRequest().authenticated()
//        .and()
//        .formLogin()
//        //指定登入頁是"/login"
//        .loginPage("/resource/login")
//        .defaultSuccessUrl("/resource/hello")//登入成功後預設跳轉到"/hello"
//        .permitAll()
//        .and()
//        .logout()
//        .logoutSuccessUrl("/resource/home")//退出登入後的預設url是"/home"
//        .permitAll();
        
        /**
         *  //允許所有使用者訪問"/"和"/home"
        http.authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                //其他地址的訪問均需驗證許可權
                .anyRequest().authenticated()
                .and()
                .formLogin()
                //指定登入頁是"/login"
                .loginPage("/login")
                .defaultSuccessUrl("/hello")//登入成功後預設跳轉到"/hello"
                .permitAll()
                .and()
                .logout()
                .logoutSuccessUrl("/home")//退出登入後的預設url是"/home"
                .permitAll();
         */
    }

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

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
                // 通過使用者名稱獲取使用者資訊
                AuthUserVo account = userService.getAuthUserInfoByMobile(name);
                if (account != null) {
                    System.out.println("account--->" + account.toString());
                    // 建立spring security安全使用者
                    User user = new User(account.getMobile(), account.getPassword(),
                            AuthorityUtils.createAuthorityList(account.getRoles()));
                    System.out.println("user--->" + user.toString());
                    return user;
                } else {
                    throw new UsernameNotFoundException("使用者[" + name + "]不存在");
                }
            }
        };

    }
}


繼承AuthorizationServerConfigurerAdapter 類的

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.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.OAuth2Authentication;
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;

/**
 * 認證授權服務端
 * @author Bruce
 *
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Value("${resource.id:spring-boot-application}") // 預設值spring-boot-application
    private String resourceId;

    @Value("${access_token.validity_period:360}") // 預設值3600
    int accessTokenValiditySeconds = 360;
    
    @Value("${access_token.validity_period:3600}")
    int refreshTokenValiditySeconds = 3600;// 30 days 2592000

    @Autowired
    private AuthenticationManager authenticationManager;

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

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");
        oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
        .withClient("normal-app")//客戶端ID
            .authorizedGrantTypes("authorization_code", "implicit")
            .authorities("ROLE_CLIENT")
            .scopes("read")//授權使用者的操作許可權
            .resourceIds(resourceId)
            .accessTokenValiditySeconds(accessTokenValiditySeconds)//token有效期為120秒
            .refreshTokenValiditySeconds(refreshTokenValiditySeconds) // 30 days
        .and()
            .withClient("trusted-app")//客戶端ID
            .authorizedGrantTypes("client_credentials", "password")
            .authorities("ROLE_TRUSTED_CLIENT")
            .scopes("read")//授權使用者的操作許可權
            .resourceIds(resourceId)
            .accessTokenValiditySeconds(accessTokenValiditySeconds)//token有效期為120秒
            .refreshTokenValiditySeconds(refreshTokenValiditySeconds) // 30 days
            .secret("secret");//密碼
        
        /**
         * // 定義了客戶端細節服務
            clients
                    .inMemory()
                    .withClient("adminClient")
                    .authorizedGrantTypes("password", "refresh_token")
                    .authorities("ADMIN")
                    .scopes("admin", "read", "write")
                    .resourceIds(RESOURCE_ID_ADMIN)
                    .secret("12345")
                    .accessTokenValiditySeconds(3600) // 1 hour
                    .refreshTokenValiditySeconds(2592000) // 30 days

                    .and()
                    .withClient("apiClient")
                    .authorizedGrantTypes("password", "refresh_token")
                    .authorities("USER")
                    .scopes("api", "read", "write")
                    .resourceIds(RESOURCE_ID_API)
                    .secret("12345")
                    .accessTokenValiditySeconds(3600) // 1 hour
                    .refreshTokenValiditySeconds(2592000) // 30 days
            ;
         */
        
        /**
         * clients.inMemory()
            .withClient("my-trusted-client")//客戶端ID
            .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
            .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
            .scopes("read", "write", "trust")//授權使用者的操作許可權
            .secret("secret")//密碼
            .accessTokenValiditySeconds(6000);//token有效期為120秒
         */
    }

    /**
     * token converter
     *
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
            /***
             * 重寫增強token方法,用於自定義一些token返回的資訊
             */
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                String userName = authentication.getUserAuthentication().getName();
                User user = (User) authentication.getUserAuthentication().getPrincipal();// 與登入時候放進去的UserDetail實現類一直檢視link{SecurityConfiguration}
                /** 自定義一些token屬性 ***/
                final Map<String, Object> additionalInformation = new HashMap<>();
                additionalInformation.put("userName", userName);
                additionalInformation.put("roles", user.getAuthorities());
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
                return enhancedToken;
            }

        };
        accessTokenConverter.setSigningKey("123");// 測試用,資源服務使用相同的字元達到一個對稱加密的效果,生產時候使用RSA非對稱加密方式
        return accessTokenConverter;
    }

    /**
     * token store
     *
     * @param accessTokenConverter
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
        return tokenStore;
    }
}

繼承ResourceServerConfigurerAdapter  類的

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.web.util.matcher.RequestMatcher;

/**
 * 資源服務
 * @author Bruce
 *
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    
    @Value("${resource.id:spring-boot-application}")
    private String resourceId;
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // @formatter:off
        resources.resourceId(resourceId);
        // @formatter:on
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // @formatter:off
            http.requestMatcher(new OAuth2RequestedMatcher())
                .authorizeRequests()
                    .antMatchers(HttpMethod.OPTIONS).permitAll()
                    .anyRequest().authenticated();
        // @formatter:on
            
            /**
             *         http.
        anonymous().disable()
        .requestMatchers().antMatchers("/user*\/**")
        .and().authorizeRequests()
        .antMatchers("/user*\/**").permitAll()
        .and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
             */
    }
    
    /**
     * 定義一個oauth2的請求匹配器
     * @author leftso
     *
     */
    private static class OAuth2RequestedMatcher implements RequestMatcher {
        @Override
        public boolean matches(HttpServletRequest request) {
            String auth = request.getHeader("Authorization");
//            //判斷來源請求是否包含oauth2授權資訊,這裡授權資訊來源可能是頭部的Authorization值以Bearer開頭,或者是請求引數中包含access_token引數,滿足其中一個則匹配成功
//            boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer");
            boolean haveAccessToken = request.getParameter("access_token")!=null;
//            return haveOauth2Token || haveAccessToken;
            return haveAccessToken;
        }
    }

}

控制器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.zhibo.xmt.api.BaseController;
import com.zhibo.xmt.api.userinfo.service.IUserService;
import com.zhibo.xmt.common.vo.AuthUserVo;

/**
 * 受保護的資源服務
 * @author Bruce
 *
 */
//@Controller
//@RequestMapping("/api/user")
@RestController
@RequestMapping("/resources")
public class AuthUserController extends BaseController {

    @Autowired
    private IUserService userService;

    /**
     *
     * @param mobile
     * @return
     */
    @PreAuthorize("hasRole('ROLE_API_USER')")
    @RequestMapping(value = "/getAuthUserInfoByMobile", method = RequestMethod.GET)
    public @ResponseBody AuthUserVo getAuthUserInfoByMobile(@RequestParam(name = "userName",required = true)String mobile) {
        AuthUserVo vo = userService.getAuthUserInfoByMobile(mobile);
        return vo;
    }
    

    /**
     * 需要使用者角色許可權
     * @return
     */
    @PreAuthorize("hasRole('ROLE_API_USER')")
    @RequestMapping(value="/user", method=RequestMethod.GET)
    public String helloUser() {
        return "hello user1111";
    }
    /**
     * 需要管理角色許可權
     *
     * @return
     */
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    @RequestMapping(value="/admin", method=RequestMethod.GET)
    public String helloAdmin() {
        return "hello admin";
    }
    /**
     * 需要客戶端許可權
     *
     * @return
     */
    @PreAuthorize("hasRole('ROLE_CLIENT')")
    @RequestMapping(value="/client", method=RequestMethod.GET)
    public String helloClient() {
        return "hello user authenticated by normal client";
    }
    /**
     * 需要受信任的客戶端許可權
     *
     * @return
     */
    @PreAuthorize("hasRole('ROLE_TRUSTED_CLIENT')")
    @RequestMapping(value="/trusted_client", method=RequestMethod.GET)
    public String helloTrustedClient() {
        return "hello user authenticated by trusted client";
    }

    @RequestMapping(value="principal", method=RequestMethod.GET)
    public Object getPrincipal() {
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return principal;
    }

    @RequestMapping(value="roles", method=RequestMethod.GET)
    public Object getRoles() {
        return SecurityContextHolder.getContext().getAuthentication().getAuthorities();
    }



}


測試上面的編碼

測試過程
步驟一:開啟瀏覽器,輸入地址
http://localhost:8080/oauth/authorize?client_id=normal-app&response_type=code&scope=read&redirect_uri=/resources/user

會提示輸入使用者名稱密碼,這時候輸入使用者名稱leftso,密碼111aaa將會出現以下介面

點選Authorize將獲取一個隨機的code,如圖:


 開啟工具postmain,輸入以下地址獲取授權token
localhost:8080/oauth/token?code=r8YBUL&grant_type=authorization_code&client_id=normal-app&redirect_uri=/resources/user
注意:url中的code就是剛才瀏覽器獲取的code值

獲取的token資訊如下圖:


這時候拿到token就可以訪問受保護的資源資訊了,如下
 
localhost:8081//resources/user

首先,直接訪問資源,會報錯401如圖:


我們加上前面獲取的access token再試:
localhost:8081//resources/user?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic3ByaW5nLWJvb3QtYXBwbGljYXRpb24iXSwidXNlcl9uYW1lIjoibGVmdHNvIiwic2NvcGUiOlsicmVhZCJdLCJyb2xlcyI6W3siYXV0aG9yaXR5IjoiUk9MRV9VU0VSIn1dLCJleHAiOjE0OTEzNTkyMjksInVzZXJOYW1lIjoibGVmdHNvIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjgxNjI5NzQwLTRhZWQtNDM1Yy05MmM3LWZhOWIyODk5NmYzMiIsImNsaWVudF9pZCI6Im5vcm1hbC1hcHAifQ.YhDJkMSlyIN6uPfSFPbfRuufndvylRmuGkrdprUSJIM

這時候我們就能成功獲取受保護的資源資訊了:


到這裡spring boot整合security oauth2 的基本使用已經講解完畢.

擴充套件思維

留下一些擴充套件
1.認證服務的客戶端資訊是存放記憶體的,實際應用肯定是不會放記憶體的,考慮資料庫,預設有個DataSource的方式,還有一個自己實現clientDetail介面方式
2.jwt這裡測試用的最簡單的對稱加密,實際應用中使用的一般都是RSA非對稱加密方式

原來文章地址:

http://www.leftso.com/blog/139.html

原GITHUB地址:

https://github.com/leftso/demo-spring-boot-security-oauth2