Spring Security Oauth2 验证和授权服务开发之MongoDB+JWT

前言

oauth2规范中具备了四种授权模式,分别如下:

·授权码模式:authorization code

·简化模式:implicit

·密码模式:resource owner password credentials

·客户端模式:client credentials

注:本示例只演示密码模式,感兴趣的同学自己花时间测试另外三种授权模式。

配置mongodb和jwt

1、新建Application入口应用类


@SpringBootApplication
@RestController
@EnableEurekaClient
// 该服务将作为OAuth2服务
@EnableAuthorizationServer
// 注意:不加@EnableResourceServer注解,下面user信息为空
@EnableResourceServer
public class Application {

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @GetMapping("/user")
    public Map<String, Object> user(OAuth2Authentication user){
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("user", user.getUserAuthentication().getPrincipal());
        userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
        return userInfo;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2、新建JWTOAuth2Config类

@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private ClientDetailsService mongoClientDetailsService;

    @Autowired
    private UserDetailsService mongoUserDetailsService;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private DefaultTokenServices tokenServices;

    // 将JWTTokenStore类中的JwtAccessTokenConverter关联到OAUTH2
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    // 自动将JWTTokenEnhancer装配到TokenEnhancer类中
    // token增强类,需要添加额外信息内容的就用这个类
    @Autowired
    private TokenEnhancer jwtTokenEnhancer;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    // /oauth/token
    // 如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走   
    //  ClientCredentialsTokenEndpointFilter来保护
    // 如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护

        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // Spring Oauth 允许开发人员挂载多个令牌增强器,因此将令牌增强器添加到TokenEnhancerChain类中
        // 设置jwt签名和jwt增强器到TokenEnhancerChain
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter));

        endpoints.tokenStore(tokenStore)
                // 在jwt和oauth2服务器之间充当翻译(签名)
                .accessTokenConverter(jwtAccessTokenConverter)
                // 令牌增强器类:扩展jwt token
                .tokenEnhancer(tokenEnhancerChain)                   
                .authenticationManager(authenticationManager)
                .userDetailsService(mongoUserDetailsService);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 使用mongodb保存客户端信息
        clients.withClientDetails(mongoClientDetailsService);
    }

}

3、新建JWTTokenEnhancer令牌增强器类

// 令牌增强器类
public class JWTTokenEnhancer implements TokenEnhancer {

    // 要进行增强需要覆盖enhance方法
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> additionalInfo = new HashMap<>();
        String newContent ="这是新加的内容";
        additionalInfo.put("newContent", newContent);

        // 所有附加的属性都放到HashMap中,并设置在传入该方法的accessToken变量上
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
        return oAuth2AccessToken;
    }
}

4、新建JWTTokenStoreConfig类以支持jwt

// 用于定义Spring将如何管理JWT令牌的创建、签名和翻译
@Configuration
public class JWTTokenStoreConfig {

    @Autowired
    private ServiceConfig serviceConfig;

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

    // @Primary作用:如果有多个特定类型bean那么就使用被@Primary标注的bean类型进行自动注入
    @Bean
    @Primary
    public DefaultTokenServices tokenServices() {
        // 用于从出示给服务的令牌中读取数据
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    // 在jwt和oauth2服务器之间充当翻译
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 定义将用于签署令牌的签名密钥(自定义 存储在git上authentication.yml文件)
        // jwt是不保密的,所以要另外加签名验证jwt token
        converter.setSigningKey(serviceConfig.getJwtSigningKey());
        return converter;
    }

    // 设置TokenEnhancer增强器中使用JWTTokenEnhancer增强器
    @Bean
    public TokenEnhancer jwtTokenEnhancer() {
        return new JWTTokenEnhancer();
    }
}

5、新建WebSecurityConfigurer类,设置访问权限、跨域CORS以及基本配置

@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Autowired
    BCryptPasswordEncoder passwordEncoder;

    // 用来处理用户验证
    // 被注入OAuth2Config类中的 endpoints方法中
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    // Spring会自动寻找同样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了
    @Autowired
    private UserDetailsService userDetailsService;

    // 定义用户、密码和用色的地方
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

    //不加这段代码不显示返回的json信息
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 设置成form登录,前端就要使用form-data传参
                // .formLogin()
                // 设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参
                .cors()
                .and()
                .httpBasic()
                .and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                // 不拦截Options请求
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // 注意:表示所有的访问都必须进行用户认证处理后才可以访问
//                .antMatchers("/register/**").permitAll()
                .anyRequest().authenticated().and()
                .csrf().disable()
                .headers()
                // 禁用缓存
                .cacheControl();
    }

    @Override
    public void configure(WebSecurity web) {
        // 注意:全局忽略访问限制
        web.ignoring().antMatchers("/register/**");
    }

    // 解决跨域CORS问题
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-auth-token"));
        configuration.setExposedHeaders(Arrays.asList("x-auth-token"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return new CorsFilter(source);
    }

}

6、新建MongoClientDetailsService类,校验及更新mongodb存储的客户端信息

@Service("mongoClientDetailsService")
public class MongoClientDetailsService implements ClientDetailsService {
    private final String CONLLECTION_NAME = "oauth_client_details";

    @Autowired
    MongoTemplate mongoTemplate;

    @Autowired
    BCryptPasswordEncoder passwordEncoder;

//    private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();

    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        BaseClientDetails client = mongoTemplate.findOne(new Query(Criteria.where("clientId").is(clientId)), BaseClientDetails.class, CONLLECTION_NAME);
        if(client==null){
            throw new RuntimeException("没有查询到客户端信息");
        }
        return client;
    }

    public void addClientDetails(ClientDetails clientDetails) {
        mongoTemplate.insert(clientDetails, CONLLECTION_NAME);
    }

    public void updateClientDetails(ClientDetails clientDetails) {
        Update update = new Update();
        update.set("resourceIds", clientDetails.getResourceIds());
        update.set("clientSecret", clientDetails.getClientSecret());
        update.set("authorizedGrantTypes", clientDetails.getAuthorizedGrantTypes());
        update.set("registeredRedirectUris", clientDetails.getRegisteredRedirectUri());
        update.set("authorities", clientDetails.getAuthorities());
        update.set("accessTokenValiditySeconds", clientDetails.getAccessTokenValiditySeconds());
        update.set("refreshTokenValiditySeconds", clientDetails.getRefreshTokenValiditySeconds());
        update.set("additionalInformation", clientDetails.getAdditionalInformation());
        update.set("scope", clientDetails.getScope());
        mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientDetails.getClientId())), update, CONLLECTION_NAME);
    }

    public void updateClientSecret(String clientId, String secret) {
        Update update = new Update();
        update.set("clientSecret", secret);
        mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientId)), update, CONLLECTION_NAME);
    }

    public void removeClientDetails(String clientId) {
        mongoTemplate.remove(new Query(Criteria.where("clientId").is(clientId)), CONLLECTION_NAME);
    }

    public List<ClientDetails> listClientDetails(){
        return mongoTemplate.findAll(ClientDetails.class, CONLLECTION_NAME);
    }
}

7、新建MongoUserDetailsService类,检验存储的用户数据

@Service
public class MongoUserDetailsService  implements UserDetailsService {

    private final String USER_CONLLECTION = "userAuth";

    @Autowired
    MongoTemplate mongoTemplate;

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // identifier:1手机号 2邮箱 3用户名 4qq 5微信 6腾讯微博 7新浪微博
        UserAuth userAuth = mongoTemplate.findOne(new Query(Criteria.where("identifier").is(username)), UserAuth.class, USER_CONLLECTION);
        if(userAuth == null) {
            throw new RuntimeException("没有查询到用户信息");
        }
        return new User(username, userAuth.getCertificate(), mapToGrantedAuthorities(userAuth.getRoles()));
    }

    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

8、实体类
User.java

@Setter
@Getter
public class User {
    private String id;

    private String uid;
    //用户名
    private String userName;
    //用户昵称
    private String nickName;
    //是否是超级管理员
    private boolean admin;
    // 性别
    private String gender;
    // 生日
    private Long birthday;
    //个性签名
    private String signature;
    //email
    private String email;
    //email
    private Long emailBindTime;
    //mobile
    private String mobile;
    //mobile
    private Long mobileBindTime;
    // 头像
    private String face;
    // 头像200*200
    private String face200;
    // 原图图像
    private String srcface;
    //状态 2正常用户 3禁言用户 4虚拟用户 5运营
    private Integer status;
    // 类型
    private Integer type;

}

UserAuth.java

@Getter
@Setter
public class UserAuth {
    //  id
    private String id;
    private String uid;
    // 1手机号 2邮箱 3用户名 4qq 5微信 6腾讯微博 7新浪微博
    private Integer identityType;
    // 手机号 邮箱 用户名或第三方应用的唯一标识
    private String identifier;
    // 密码凭证(站内的保存密码,站外的不保存或保存token)
    private String certificate;
    // md5 盐值加密
    private String md5;

    //角色ID
    private List<String> roles;
}

9、表结构

oauth_client_details:
{
    "_id" : ObjectId("5f01e1cf1315d14f5bea1679"),
    "clientId" : "core-resource",
    "resourceIds" : "card",
    "clientSecret" : "$2a$10$8NUXEVgWW72Gf.QQtQlsQu1L9KGxAonW.QfO3s82Kr9DADL4wn24K",
    "authorizedGrantTypes" : "password,authorization_code,refresh_token",
    "registeredRedirectUris" : "http://localhost:9001/base/login",
    "authorities" : "",
    "accessTokenValiditySeconds" : "7200",
    "refreshTokenValiditySeconds" : "0",
    "autoapprove" : true,
    "additionalInformation" : null,
    "scope" : "all"
}

user:
{
    "_id" : ObjectId("5e7d56c9b03e9a046ab26cac"),
    "uid" : "5e7d56c9b03e9a046ab26ca9",
    "username" : "zhangwei",
    "admin" : false,
    "email" : "zhangwei900808@126.com",
    "emailBindTime" : NumberLong(1585272521646),
    "status" : 2,
    "type" : 1
}

userAuth:
{
    "_id" : ObjectId("5e7d56c9b03e9a046ab26caa"),
    "uid" : "5e7d56c9b03e9a046ab26ca9",
    "identityType" : 2,
    "identifier" : "zhangwei900808@126.com",
    "certificate" : "$2a$10$OdHuIooHSv60jC7YYahQB.dsfAWFA..Jdb0KwRcn9F9yrz64HPFfC",
    "roles" : [ 
        "ROLE_USER"
    ]
}
{
    "_id" : ObjectId("5e7d56c9b03e9a046ab26cab"),
    "uid" : "5e7d56c9b03e9a046ab26ca9",
    "identityType" : 3,
    "identifier" : "zhangwei",
    "certificate" : "$2a$10$nHqwjbwAjgHeTu3.FDSDWEWe6fa/7zcFZ6bVSrrkGkEZ7OIYOdkMe",
    "roles" : [ 
        "ROLE_USER"
    ]
}

演示

image.png

总结:

1、oauth2保存客户端信息有好多种:内存,jdbc。像我这里使用的是mongodb
2、数据库表User保存的是用户基本信息,真正密码和访问类型(用户名、邮箱、手机号等等)是在表UserAuth里面,这个大家注意下
3、在WebSecurityConfigurerAdapter类中,设置成form登录,前端就要使用form-data传参,设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参
4、AuthorizationServerConfigurerAdapter中要设置获取token的路由/oauth/token能被访问到,还要设置成下面这段代码:

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

5、MongoUserDetailsService最后返回的是org.springframework.security.core.userdetails.UserDetails类,所以我们从mongodb返回自己的UserAuth之后再转换成上面的类即可
6、WebSecurityConfigurerAdapter下的configure(HttpSecurity http)方法和configure(WebSecurity web)忽略网址本质上不同,

configure(HttpSecurity http)方法是忽略已经校验过的用户,限制它的某些权限比如:POST、GET或者只有USER或者ADMIN用户才能访问

configure(WebSecurity web)则是全局忽略,比如说静态文件,比如说注册页面)、全局HttpFirewall配置、是否debug配置、全局SecurityFilterChain配置、privilegeEvaluator、expressionHandler、securityInterceptor、HttpSecurity
具体的权限控制规则配置

@Override

protected void configure(HttpSecurity http) throws Exception {
    http
            // 设置成form登录,前端就要使用form-data传参
            // .formLogin()
            // 设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参
            .httpBasic()
            .and()
            // 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            .authorizeRequests()
            // 注意:表示所有的访问都必须进行用户认证处理后才可以访问
            // .antMatchers("/register/**").permitAll()
            .anyRequest().authenticated()
            .and().csrf().disable().cors();
}

@Override
public void configure(WebSecurity web) {
    // 注意:全局忽略访问限制
    web.ignoring().antMatchers("/register/**");
}

引用

Spring Security Oauth2 授权服务开发之MongoDB
解决Spring Security OAuth在访问/oauth/token时候报401 authentication is required
Spring cloud oauth2 研究--oauth_client_detail表说明
关于spring boot security设置忽略地址不生效问题

阅读 439

推荐阅读
全栈工程师进阶
用户专栏

日常学习总结与分享,包括:前端、后台与运维,讲解的知识点包括:javascript、vuejs、reactjs、springb...

78 人关注
39 篇文章
专栏主页