Oauth2.0实战-授权服务和受保护资源服务

247 阅读3分钟

前段时间写了一篇Oauth2.0的基础,今天我们来进行项目实战

授权服务

首先来看授权服务,先引用对应的包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
  • 配置Spring Security 新建类WebSecurityConfig 继承 WebSecurityConfigurerAdapter,并添加@Configuration @EnableWebSecurity注解,重写三个方法,代码如下:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserServiceDetail userServiceDetail;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
        //内存存储
//        auth
//                .inMemoryAuthentication()
//                .passwordEncoder(passwordEncoder())
//                .withUser("user")
//                .password(passwordEncoder().encode("user"))
//                .roles("USER");

    }


    /**
     * 配置了默认表单登陆以及禁用了 csrf 功能,并开启了httpBasic 认证
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http    // 配置登陆页/login并允许访问
                .formLogin().permitAll()
                // 登出页
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                // 其余所有请求全部需要鉴权认证
                .and().authorizeRequests().anyRequest().authenticated()
                // 由于使用的是JWT,我们这里不需要csrf
                .and().csrf().disable();
    }

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

其中我们指定了通过UserServiceDetail来进行用户验证,再来看一下UserServiceDetail类:

@Service
public class UserServiceDetail implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
        userQueryWrapper.eq("username", s);
        User user = userMapper.selectOne(userQueryWrapper);
        if (user == null) {
            throw new RuntimeException("用户名或密码错误");
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        List<Role> roles = userMapper.selectRoleByUserId(user.getId());
        roles.forEach(role->{
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        });
        return new UserModel(user.getUserName(),user.getPassword(),authorities);
    }
}

该类实现UserDetailsService接口,重写loadUserByUsername方法,返回对象为UserDetails,我们可以自己来实现UserDetails,本例我们通过UserModel来实现,loadUserByUsername方法我们实现了用户登录校验,校验成功后查询相关权限,封装为authorities

  • 配置认证服务器,代码如下:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    //从WebSecurityConfig加载
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private DataSource dataSource;
    /**
     * 配置客户端详细信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //从数据库读取
        clients.jdbc(dataSource);
        /*clients.inMemory()
                //客户端ID
                .withClient("zcs")
                .secret(new BCryptPasswordEncoder().encode("zcs"))
                //权限范围
                .scopes("app")
                .resourceIds()
                //授权码模式
                .authorizedGrantTypes("authorization_code")
                .redirectUris("www.baidu.com");*/
    }

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

    /**
     * 在令牌端点定义安全约束
     * 允许表单验证,浏览器直接发送post请求即可获取tocken
     * 这部分写这样就行
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 开启/oauth/token_key验证端口无权限访问
                .tokenKeyAccess("permitAll()")
                // 开启/oauth/check_token验证端口认证权限访问
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }
    /**
     * 非对称加密算法对token进行签名
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        final JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
        // 导入证书
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "mypass".toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2"));
        return converter;
    }
    /**
     * 使用JWT令牌存储
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    public static void main(String[] args) {
        System.out.println(new BCryptPasswordEncoder().encode("zcs"));
    }
}

configure(ClientDetailsServiceConfigurer clients)我们指定了从数据库读取,方便维护,对应oauth_client_details表,客户端验证时会直接从库中匹配验证

configure(AuthorizationServerEndpointsConfigurer endpoints)我们使用JWT来产生令牌,然后我们通过CustomJwtAccessTokenConverter完成JWt令牌增强,令牌中增加用户信息

/**
 * 自定义添加额外token信息
 */
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
        Map<String, Object> additionalInfo = new HashMap<>();
        UserModel user = (UserModel)authentication.getPrincipal();
        additionalInfo.put("USER",user);
        defaultOAuth2AccessToken.setAdditionalInformation(additionalInfo);
        return super.enhance(defaultOAuth2AccessToken,authentication);
    }
}

按照Oauth2规范,传递的JWT令牌必须加密,所以我们采用了非对称加密的方式,具体生成方式如下:

keytool -genkeypair -alias oauth2 -keyalg RSA -keypass mypass -keystore oauth2.jks -storepass mypass

服务器执行该命令会生成oauth.jks秘钥文件,我们存放在resource目录

受保护资源服务

再来搭建受保护资源服务,引入oauth和web依赖

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>	

继承ResourceServerConfigurerAdapter类,代码如下:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true) //启用方法注解方式来进行权限控制
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    /**
     * 声明了资源服务器的ID是userservice,声明了资源服务器的TokenStore是JWT
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("userservice").tokenStore(tokenStore());
    }

    /**
     * 配置TokenStore
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 配置公钥
     * @return
     */
    @Bean
    protected JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        Resource resource = new ClassPathResource("public.cert");
        String publicKey = null;
        try {
            publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
        } catch (IOException e) {
            e.printStackTrace();
        }
        converter.setVerifierKey(publicKey);
        return converter;
    }

    /**
     * 配置了除了/user路径之外的请求可以匿名访问
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .anyRequest().permitAll();
    }
}

前面我们生成了密钥,所以这一块我们需要配置公钥进行解密,相关命令如下:

keytool -exportcert -file pub.crt -keystore oauth2.jks -alias oauth2

服务器执行该命令会产生公钥,我们粘贴在resource下的public.cert

返回如下:

拿token进行资源请求访问:

  • 注意: 因为我们校验client_serect指定了加密,所以库中密码需要存储加密后的:

完整代码

github.com/skyemin/oau…