美文网首页安全认证springSpringBoot
spring security oauth2 jwt 示例

spring security oauth2 jwt 示例

作者: 吹奏一池春水 | 来源:发表于2019-03-14 23:11 被阅读176次

    公司项目是基于dubbo和zookeeper在门户项目端提供接口统一鉴权的,一直没有接触过spring security。出于好奇一直想了解下spring security怎么使用以及oauth2和jwt的鉴权过程,这次学习过程中将spring security搭上oauth2和jwt做一个示例程序。

    OAuth 2.0

    OAuth 2.0是一套关于授权的开放标准,主要针对第三方应用认证授权场景,详细信息可参考RFC 6749
    OAuth 2.0定义了四个角色:

    • resource owner:资源所有者,即用户本身
    • resource server:资源服务器,服务提供商存放用户应用资源的服务器
    • client:客户端,即第三方应用
    • authorization server:授权服务器,服务提供商专门处理认证授权的服务器

    打个比方,我打开简书发现没有账号但又懒得注册,点击使用QQ账号登录,那么这里的就是resource owner简书clientQQ保存账号信息比如头像的服务器就是resource serverQQ用来处理登录授权的服务器就是authorization server,如果以上操作按照OAuth 2.0的标准,它的流程大致如下:

    OAuth 2.0授权流程

    其中关键就是授权码token的获取,对于token的获取OAuth 2.0定义了四种方式:

    • Authorization Code:授权码模式,例如微信上给第三方小程序授权
    • Implicit:简化模式,授权码模式的简化版本,嫌授权码模式麻烦时用
    • Resource Owner Password Credentials:资源所有者密码凭证模式,一般高信任的内部应用间使用
    • Client Credentials:客户端凭证模式,一般用于开放API调用

    JWT

    即JSON Web Token的缩写,json格式的token,包含三部分:

    • Header:头部,包含类型和签名算法名称
    • Payload:载荷,主要内容,包含需要传递的信息
    • Signature:签名,对头部和载荷哈希后的内容

    使用JWT主要是服务器端不需要存储token,资源服务器也不需要访问授权服务器验证token,每次访问只需对应资源服务器验证token有效性,减少服务器开销。

    Spring security

    Spring security框架基于OAuth 2.0做了相关实现,我们只需要做相应的配置就能实现一个基于OAuth 2.0和JWT的授权服务。

    引入相关依赖

    这里是使用gradle做依赖管理

    dependencies {
        compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.1.3.RELEASE'
        compile group: 'org.springframework.boot', name: 'spring-boot-starter-jdbc', version: '2.1.3.RELEASE'
        compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.1.3.RELEASE'
        compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-security', version: '2.1.1.RELEASE'
        compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-oauth2', version: '2.1.1.RELEASE'
        compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.8.1'
        compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
        compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8'
    
        runtime group: 'com.alibaba', name: 'druid', version: '1.1.13'
        runtime group: 'mysql', name: 'mysql-connector-java', version: '5.1.47'
    }
    

    配置spring security

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserDetailsService authUserDetailsService;
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(authUserDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable().authorizeRequests().antMatchers("/oauth/**").permitAll();
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    

    配置授权服务器

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private TokenStore jwtTokenStore;
    
        @Autowired
        private JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Autowired
        @Qualifier("jwtTokenEnhancer")
        private TokenEnhancer jwtTokenEnhancer;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            //允许表单认证
            security.allowFormAuthenticationForClients();
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory().withClient("client_admin")
                    .resourceIds("resource")
                    .authorizedGrantTypes("password", "client_credentials", "refresh_token")
                    .scopes("read")
                    .authorities("admin")
                    .secret(passwordEncoder.encode("123456"))
                    .accessTokenValiditySeconds(3 * 60 * 60)
                    .refreshTokenValiditySeconds(6 * 60 * 60);
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> enhancerList = new ArrayList<>();
            enhancerList.add(jwtTokenEnhancer);
            enhancerList.add(jwtAccessTokenConverter);
            enhancerChain.setTokenEnhancers(enhancerList);
    
            endpoints.authenticationManager(authenticationManager)
                    .tokenStore(jwtTokenStore)
                    .accessTokenConverter(jwtAccessTokenConverter)
                    .tokenEnhancer(enhancerChain)
                    .exceptionTranslator(loggingExceptionTranslator());
        }
    
        @Bean
        public WebResponseExceptionTranslator loggingExceptionTranslator() {
            return new DefaultWebResponseExceptionTranslator() {
                private Log log = LogFactory.getLog(getClass());
    
                @Override
                public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
                    //异常堆栈信息输出
                    log.error("异常堆栈信息", e);
                    return super.translate(e);
                }
            };
        }
    }
    

    配置资源服务器

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        @Autowired
        private ResourceServerTokenServices resourceJwtTokenServices;
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            //resource资源只允许基于令牌的身份验证
            resources.resourceId("resource").stateless(true);
            resources.tokenServices(resourceJwtTokenServices);
        }
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    // 由于我们希望在用户界面中访问受保护的资源,因此我们需要允许创建会话
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .and()
                    .requestMatchers().anyRequest()
                    .and()
                    //启用匿名登录,不可访问受保护资源
                    .anonymous()
                    .and()
                    .authorizeRequests()
                    //配置protected访问控制,必须认证过后才可以访问
                    .antMatchers("/protected/**").authenticated();
        }
    }
    

    JWT配置

    @Configuration
    public class JwtTokenConfig {
    
        private static KeyPair KEY_PAIR;
    
        //此处只有在授权服务器和资源服务器在一起的时候才能这样搞,实际使用RSA还是需要用JDK和openssl去生成证书
        static {
            try {
                KEY_PAIR = KeyPairGenerator.getInstance("RSA").generateKeyPair();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
        }
    
    
        @Autowired
        private SystemAccountDao systemAccountDao;
    
        @Bean
        public TokenStore jwtTokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
            accessTokenConverter.setKeyPair(KEY_PAIR);
            return accessTokenConverter;
        }
    
        @Bean
        public ResourceServerTokenServices resourceJwtTokenServices() {
            final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            // 使用自定义的Token转换器
            defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
            // 使用自定义的tokenStore
            defaultTokenServices.setTokenStore(jwtTokenStore());
            return defaultTokenServices;
        }
    
        /**
         * token信息扩展
         */
        @Bean
        public TokenEnhancer jwtTokenEnhancer() {
            return new TokenEnhancer() {
                @Override
                public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                    Authentication userAuthentication = authentication.getUserAuthentication();
                    if (userAuthentication != null) {
                        String userName = userAuthentication.getName();
                        List<SystemAccount> list = systemAccountDao.findByAccount(userName);
                        if (list != null && !list.isEmpty()) {
                            SystemAccount account = list.get(0);
                            Map<String, Object> additionalInformation = new HashMap<>();
                            Map<String, String> map = new HashMap<>();
                            map.put("account", account.getAccount());
                            map.put("createTime", account.getCreateTime().toString());
                            map.put("state", String.valueOf(account.getState()));
                            additionalInformation.put("user", GsonUtil.toJson(map));
                            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                        }
                    }
                    return accessToken;
                }
            };
        }
    }
    

    配置UserService

    @Component
    public class AuthUserDetailsService implements UserDetailsService {
    
        @Autowired
        private SystemAccountDao systemAccountDao;
    
        @Autowired
        private SystemAccountRoleDao systemAccountRoleDao;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            List<SystemAccount> list = systemAccountDao.findByAccount(username);
            if (list != null && !list.isEmpty()) {
                SystemAccount account = list.get(0);
                List<SystemAccountRole> roleList = systemAccountRoleDao.findByAccountId(account.getId());
                Collection<SimpleGrantedAuthority> authorities = new HashSet<>();
                if (roleList != null && !roleList.isEmpty()) {
                    for (SystemAccountRole accountRole : roleList) {
                        authorities.add(new SimpleGrantedAuthority(String.valueOf(accountRole.getRoleId())));
                    }
                }
                return new User(username, account.getPassword(),
                        account.getState() == 1, true, true, true,
                        authorities);
            }
            return null;
        }
    }
    

    password模式访问示例

    password模式访问

    客户端模式访问示例

    客户端模式访问

    携带token访问受保护资源

    携带token访问受保护资源

    项目地址

    示例项目代码可到我的Github上下载:https://github.com/DexterQY/authentication

    相关文章

      网友评论

        本文标题:spring security oauth2 jwt 示例

        本文链接:https://www.haomeiwen.com/subject/rwttmqtx.html