乐趣区

Spring-Security-Oauth2-验证和授权服务开发之MongoDBJWT

前言

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 类,设置访问权限以及基本配置

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

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

    // Spring 会自动寻找同样类型的具体类注入,这里就是 MongoUserDetailsService 了
    @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 传参
                .httpBasic()
                .and()
                .authorizeRequests()
                .antMatchers("/register").permitAll()
                .anyRequest()
                .authenticated()
                .and().csrf().disable().cors();
    }
}

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.k2JPUo3..Jdb0KwRcn9F9yrz64HPFfC",
    "roles" : ["ROLE_USER"]
}
{"_id" : ObjectId("5e7d56c9b03e9a046ab26cab"),
    "uid" : "5e7d56c9b03e9a046ab26ca9",
    "identityType" : 3,
    "identifier" : "zhangwei",
    "certificate" : "$2a$10$nHqwjbwAjgHeTu3.lunKVuVe6fa/7zcFZ6bVSrrkGkEZ7OIYOdkMe",
    "roles" : ["ROLE_USER"]
}

演示

总结:

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();}

引用

Spring Security Oauth2 授权服务开发之 MongoDB
解决 Spring Security OAuth 在访问 /oauth/token 时候报 401 authentication is required
Spring cloud oauth2 研究 –oauth_client_detail 表说明

退出移动版