前言

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

·授权码模式:authorization code

·简化模式:implicit

·密码模式:resource owner password credentials

·客户端模式:client credentials

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

配置mongodb和jwt

1、新建Application入口应用类

@SpringBootApplication@RestController@EnableEurekaClient// 该服务将作为OAuth2服务@EnableAuthorizationServer// 注意:不加@EnableResourceServer注解,下面user信息为空@EnableResourceServerpublic 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类

@Configurationpublic 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令牌的创建、签名和翻译@Configurationpublic 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类,设置访问权限以及基本配置

@Configurationpublic 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类,检验存储的用户数据

@Servicepublic 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@Getterpublic 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@Setterpublic 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表说明