前言

项目基于springcloud,授权决定使用提供的spring-oauth2,了解了oauth2原理之后,基于业务有很多需要重写的点;

1.获取token同时需要获取客户端信息(手机型号,app版本号等)

2.除了提供的password方式登录,还需要提供验证码登录功能

源码理解

对spring-cloud-starter-oauth2包的源码进行了简单的了解:

endpoint: 端点,/oauth/token的接口地址 TokenEndPoint是接口的逻辑

spring authorization server 认证完整流程 spring cloud 认证授权_oauth

TokenGranter:token授权,默认提供了4种granter(RefreshTokenGranter,ImplicitTokenGranter,ResourceOwnerPasswordTokenGranter,ClientCredentialsTokenGranter)

自定义TokenGranter可以实现获取除了默认参数的其他参数,同时可以增加验证

AuthenticationProvider:身份验证提供者,用于验证身份,密码的匹配等都是再这通过service获取用户信息进行校验


DaoAuthenticationProvider是默认的实现类,通过重写provider来自定义校验逻辑


UserDetailsService:用户信息查询

 

实现

第一步:指定自定义provider

spring authorization server 认证完整流程 spring cloud 认证授权_token_02

自定义granter

package com.mlines.cloud.granter;

import com.mlines.cloud.token.AppAuthenticationToken;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 短信验证码方式
 * zhangmx
 */

public class SMSCodeTokenGranter extends AbstractTokenGranter {
    private static final String GRANT_TYPE = "smscode";

    private final AuthenticationManager authenticationManager;

    public SMSCodeTokenGranter(AuthenticationManager authenticationManager,
                               AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    protected SMSCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
                                  ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }


    /**
     * 在这个方法可以进行验证码等其他操作
     * @param client
     * @param tokenRequest
     * @return
     */
    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
        String username = parameters.get("username");
        String smscode = parameters.get("smscode");
        String deviceId=parameters.get("deviceId");
        String type=parameters.get("type");
        String sysVersion=parameters.get("sysVersion");
        String deviceVersion=parameters.get("deviceVersion");
        String appVersion=parameters.get("appVersion");
        parameters.put("loginType","sms");
        // Protect from downstream leaks of password
        parameters.remove("password");

        //校验验证码
        if(!StringUtils.equals(smscode,"1001")){
            throw new InvalidGrantException("验证码不正确");
        }

        Authentication userAuth = new AppAuthenticationToken(username, smscode,type,deviceId,sysVersion,deviceVersion,appVersion);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            userAuth = authenticationManager.authenticate(userAuth);
        }
        catch (AccountStatusException ase) {
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        }
        catch (BadCredentialsException e) {
            // If the username/password are wrong the spec says we should send 400/invalid grant
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

第二步:指定自定义provider

spring authorization server 认证完整流程 spring cloud 认证授权_springcloud_03

自定义provider:

package com.mlines.cloud.provider;


import com.mlines.cloud.feign.client.SysUserClient;
import com.mlines.cloud.token.AppAuthenticationToken;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;

import java.util.Map;

/**
 * Created by fp295 on 2018/11/25.4
 * 用户名密码登录
 */
public class AppAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private SysUserClient sysUserClient;

    private PasswordEncoder passwordEncoder;
//    /**
//     * 根据用户名取回用户信息后,进入此方法判断密码是否匹配,如果是验证码登录 判断验证码是否正确
//     * @param var1
//     * @param authentication
//     * @throws AuthenticationException
//     */
//    @Override
//    protected void additionalAuthenticationChecks(UserDetails var1, Authentication authentication) throws AuthenticationException {
//
//        if(authentication.getCredentials() == null) {
//            this.logger.debug("Authentication failed: no credentials provided");
//            throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad credentials"));
//        } else {
//            String presentedPassword = authentication.getCredentials().toString();
//            Map<String,Object> otherParams=(Map<String,Object>)authentication.getDetails();//其他参数
//            String loginType=(String)otherParams.get("loginType");
//            if(StringUtils.equals(loginType,"password")){//密码方式登录
//                if (!passwordEncoder.matches(presentedPassword, var1.getPassword())) {
//                    logger.debug("密码错误");
//
//                    throw new BadCredentialsException(messages.getMessage(
//                            "400",
//                            "密码错误"));
//                }
//            }
//

            // 验证码验证,调用公共服务查询 key 为authentication.getPrincipal()的value, 并判断其与验证码是否匹配
            if(!"1000".equals(presentedPassword)){
                this.logger.debug("Authentication failed: verifyCode does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad verifyCode"));
            }
//        }
//    }
//
//
//
//    @Override
//    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user,String deviceId) {
//        AppAuthenticationToken result = new AppAuthenticationToken(principal, authentication.getCredentials(), user.getAuthorities(),deviceId);
//        result.setDetails(authentication.getDetails());
//        return result;
//    }
//
//    @Override
//    protected UserDetails retrieveUser(String phone, Authentication authentication) throws AuthenticationException {
//        UserDetails loadedUser;
//        try {
//            loadedUser = userDetailsService.loadUserByUsername(phone);
//        } catch (UsernameNotFoundException var6) {
//            throw var6;
//        } catch (Exception var7) {
//            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
//        }
//
//        if(loadedUser == null) {
//            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
//        } else {
//            return loadedUser;
//        }
//    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if(authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            Map<String,Object> otherParams=(Map<String,Object>)authentication.getDetails();//其他参数
            String loginType=(String)otherParams.get("loginType");
            if(StringUtils.equals(loginType,"password")){//密码方式登录
                if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                    logger.debug("密码错误");

                    throw new BadCredentialsException(messages.getMessage(
                            "400",
                            "密码错误"));
                }
            }

//
//            // 验证码验证,调用公共服务查询 key 为authentication.getPrincipal()的value, 并判断其与验证码是否匹配
//            if(!"1000".equals(presentedPassword)){
//                this.logger.debug("Authentication failed: verifyCode does not match stored value");
//                throw new BadCredentialsException(this.messages.getMessage("AppAuthenticationProvider.badCredentials", "Bad verifyCode"));
//            }
        }
    }

    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;
        try {
            loadedUser = userDetailsService.loadUserByUsername(username);
        } catch (UsernameNotFoundException var6) {
            throw var6;
        } catch (Exception var7) {
            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
        }

        if(loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return AppAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return passwordEncoder;
    }

//
//
    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

自定义service

spring authorization server 认证完整流程 spring cloud 认证授权_spring_04

 

其他代码片段

package com.mlines.cloud.config;

import com.mlines.cloud.granter.AppResourceOwnerPasswordTokenGranter;
import com.mlines.cloud.granter.SMSCodeTokenGranter;
import com.mlines.cloud.handler.CustomWebResponseExceptionTranslator;
import com.mlines.cloud.provider.AppAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;
    @Autowired
    private CustomWebResponseExceptionTranslator customWebResponseExceptionTranslator;
//    @Autowired
//    private UsernameUserDetailService userDetailsService;
    @Autowired
    private UserDetailsService userDetailsService;//读取客户端的service  app可以是一个客户端    web可以是一个客户端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("web-app")
                .secret( new BCryptPasswordEncoder().encode("rq9nuNkIwT"))
                .scopes("web-app")
                .authorizedGrantTypes("password","smscode", "refresh_token")
                .accessTokenValiditySeconds(86400)//超时
                .refreshTokenValiditySeconds(604800)
                .and()
                .withClient("android-app")
                .secret(new BCryptPasswordEncoder().encode("ijnuybdsvv"))
                .scopes("android-app")
                .authorizedGrantTypes("password","smscode", "refresh_token")
                .accessTokenValiditySeconds(86400*7)
                .refreshTokenValiditySeconds(604800*4);
    }

    @Bean
    public UserDetailsService userDetailsService(){
        return userDetailsService;
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey("cloudserver");
        return jwtAccessTokenConverter;
    }

    public static void main(String[] args){
//        Jwt jwt=JwtHelper.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiLnmbvlvZXmiJDlip8iLCJjb2RlIjoyMDAsImRhdGEiOnsibGFiZWwiOiLlvKDlrablj4siLCJpZCI6Mn0sInVzZXJfbmFtZSI6IjE2NjY2NjY2NjIxIiwic2NvcGUiOlsid2ViLWFwcCJdLCJleHAiOjE1NTA3NDc1NTQsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiJhZDk1OGZhMC1mYWNiLTQyOGQtYWZlMi0xNGQ0OGJhMTBjMjAiLCJjbGllbnRfaWQiOiJ3ZWItYXBwIn0.yQbgx8rXCkwqdYWhGOBIRJZyIe9VOKrV3yNIcqVE294");
//        System.out.println(new String(jwt.getClaims().getBytes()));
        System.out.println(new BCryptPasswordEncoder().encode("123456"));
        System.out.println(new BCryptPasswordEncoder().matches("123456","$2a$10$xT8XiML30Yer0d3cYBmXfewMgZtQyiGc25zZs9ipkvKJuyOQDwcF2"));
    }

    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(), jwtAccessTokenConverter()));
        endpoints.tokenStore(jwtTokenStore())
                .accessTokenConverter(jwtAccessTokenConverter())
                .reuseRefreshTokens(false)//该字段设置设置refresh token是否重复使用,true:reuse;false:no reuse.
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenEnhancer(enhancerChain);
        endpoints.exceptionTranslator(customWebResponseExceptionTranslator);
        endpoints.tokenGranter(new CompositeTokenGranter(getTokenGranters(endpoints)));
    }

    private List<TokenGranter> getTokenGranters(AuthorizationServerEndpointsConfigurer endpoints){
        ClientDetailsService clientDetails = endpoints.getClientDetailsService();
        AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
        AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
        OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
//        ((DefaultTokenServices)tokenServices).setAuthenticationManager(new ProviderManager(getProvider(),null));
        List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
                requestFactory));
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
        tokenGranters.add(implicit);
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
        if (authenticationManager != null) {
//            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
//                    clientDetails, requestFactory));//有了自定义的用户名密码验证方式不需要初始化默认的了
            tokenGranters.add(new AppResourceOwnerPasswordTokenGranter(new ProviderManager(getProvider(),null),tokenServices,endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory()));
            tokenGranters.add(new SMSCodeTokenGranter(new ProviderManager(getProvider(),null),tokenServices,endpoints.getClientDetailsService(),endpoints.getOAuth2RequestFactory()));

        }
        //添加自定义granter
        return tokenGranters;
    }

    private List<AuthenticationProvider> getProvider(){
        List<AuthenticationProvider> list=new ArrayList<>();
        AppAuthenticationProvider provider = new AppAuthenticationProvider();
        // 设置userDetailsService
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(new BCryptPasswordEncoder());
        // 禁止隐藏用户未找到异常
        provider.setHideUserNotFoundExceptions(false);
        list.add(provider);
        return list;
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients()
                .authenticationEntryPoint(new AuthExceptionEntryPoint())//自定义异常信息
                .accessDeniedHandler(customAccessDeniedHandler)
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }



    @Bean
    public TokenEnhancer customTokenEnhancer() {
        return new CustomTokenEnhancer();// 写入自定义信息
    }
//
//    @Override
//    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
//        oauthServer.authenticationEntryPoint(new AuthExceptionEntryPoint());
//    }

}
package com.mlines.cloud.config;

import com.mlines.cloud.filter.AppLoginAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
//
    @Autowired
    private UserDetailsService userDetailsService;

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

//    @Override
//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth
//                .userDetailsService(userDetailsService())
//                .passwordEncoder(passwordEncoder());
//    }




    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //TODO:use md5
//        auth.authenticationProvider(appAuthenticationProvider());
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());

    }
    //不定义没有password grant_type
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
//
    @Bean
    public AppLoginAuthenticationFilter getPhoneLoginAuthenticationFilter() {
        AppLoginAuthenticationFilter filter = new AppLoginAuthenticationFilter();
        try {
            filter.setAuthenticationManager(this.authenticationManagerBean());
        } catch (Exception e) {
            e.printStackTrace();
        }
//        filter.setAuthenticationSuccessHandler(new LoginAuthSuccessHandler());
        return filter;
    }
//    /**
//     * 手机验证码登陆过滤器
//     * @return
//     */
//    @Bean
//    public UserNamePwdAuthenticationFilter getUserNamePwdFilter() {
//        UserNamePwdAuthenticationFilter filter = new UserNamePwdAuthenticationFilter();
//        try {
//            filter.setAuthenticationManager(this.authenticationManagerBean());
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
//        return filter;
//    }

}