Oauth2 基于redis的认证服务器demo

Oauth2 基于redis的可集群认证服务器demo

功能点

  • 支持授权码、账号密码、短信验证码模式获取token
  • 授权码、短信验证码基于redis存储
  • 刷新token
  • 对springSecurity内部的认证机制进行横向优雅扩展

添加依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.4.RELEASE</version>
        <relativePath/>
</parent>
    
<dependencies>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.security.oauth</groupId>
        <artifactId>spring-security-oauth2</artifactId>
    </dependency>
    
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session</artifactId>
    </dependency>
        

</dependencies>

在创建认证服务前,我们先定义一个MyUser对象


public class MyUser implements Serializable {
    private static final long serialVersionUID = 3497935890426858541L;

    private String userName;
    private String password;
    private boolean accountNonExpired = true;
    private boolean accountNonLocked= true;
    private boolean credentialsNonExpired= true;
    private boolean enabled= true;

    // get set 略
}



定义UserDetailService实现

public class MyUserDetailService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUser user = new MyUser();
        user.setUserName(username);
        user.setPassword(this.passwordEncoder.encode("123456"));
        return new User(username, user.getPassword(), user.isEnabled(),
                user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

这里的逻辑是用什么账号登录都可以,但是密码必须为123456,并且拥有”admin”权限


定义消息渲染类,类似于在Filter做到MVC返回一个实体到前端解析成josn的功能

public class EntityResponseRenderer {

	private final Log logger = LogFactory.getLog(EntityResponseRenderer.class);

	private List<HttpMessageConverter<?>> messageConverters = geDefaultMessageConverters();

	public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
		this.messageConverters = messageConverters;
	}

	public void writeHttpEntityResponse(Object responseObj,
										HttpStatus httpStatus,
										ServletRequest request, ServletResponse response) {
		try {
			HttpHeaders headers = new HttpHeaders();
			ResponseEntity responseEntity=new ResponseEntity(responseObj,headers, httpStatus);
			HttpServletRequest req = (HttpServletRequest) request;
			HttpServletResponse res = (HttpServletResponse) response;
			ServletWebRequest webRequest=new ServletWebRequest(req, res);
			if (responseEntity == null) {
				return;
			}
			HttpInputMessage inputMessage = createHttpInputMessage(webRequest);
			HttpOutputMessage outputMessage = createHttpOutputMessage(webRequest);
			if (responseEntity instanceof ResponseEntity && outputMessage instanceof ServerHttpResponse) {
				((ServerHttpResponse) outputMessage).setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode());
			}
			HttpHeaders entityHeaders = responseEntity.getHeaders();
			if (!entityHeaders.isEmpty()) {
				outputMessage.getHeaders().putAll(entityHeaders);
			}
			Object body = responseEntity.getBody();
			if (body != null) {
				writeWithMessageConverters(body, inputMessage, outputMessage);
			}
			else {
				// flush headers
				outputMessage.getBody();
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void writeWithMessageConverters(Object returnValue, HttpInputMessage inputMessage,
			HttpOutputMessage outputMessage) throws IOException, HttpMediaTypeNotAcceptableException {
		List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
		if (acceptedMediaTypes.isEmpty()) {
			acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
		}
		MediaType.sortByQualityValue(acceptedMediaTypes);
		Class<?> returnValueType = returnValue.getClass();
		List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
		for (MediaType acceptedMediaType : acceptedMediaTypes) {
			for (HttpMessageConverter messageConverter : messageConverters) {
				if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
					messageConverter.write(returnValue, acceptedMediaType, outputMessage);
					if (logger.isDebugEnabled()) {
						MediaType contentType = outputMessage.getHeaders().getContentType();
						if (contentType == null) {
							contentType = acceptedMediaType;
						}
						logger.debug("Written [" + returnValue + "] as \"" + contentType + "\" using ["
								+ messageConverter + "]");
					}
					return;
				}
			}
		}
		for (HttpMessageConverter messageConverter : messageConverters) {
			allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
		}
		throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);
	}

	private List<HttpMessageConverter<?>> geDefaultMessageConverters() {
		List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>();
		result.addAll(new RestTemplate().getMessageConverters());
		result.add(new JaxbOAuth2ExceptionMessageConverter());
		return result;
	}

	private HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
		HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
		return new ServletServerHttpRequest(servletRequest);
	}

	private HttpOutputMessage createHttpOutputMessage(NativeWebRequest webRequest) throws Exception {
		HttpServletResponse servletResponse = (HttpServletResponse) webRequest.getNativeResponse();
		return new ServletServerHttpResponse(servletResponse);
	}

}

定义基于redis的授权码存储器

/**
 * 默认的授权码存储为InMemoryAuthorizationCodeServices, 不适用于微服务场景, 需要
 * 自定义一个基于redis的授权码存储器
 * @program: spring-security-demo
 * @description:
 * @author: chenzejie
 * @create: 2019-09-04 13:49
 **/
public class InRedisAuthorizationCodeServices  extends RandomValueAuthorizationCodeServices {
    private RedisTemplate<String,OAuth2Authentication> redisTemplate;
    private String prefix="authorization:code:";
    public InRedisAuthorizationCodeServices(RedisTemplate<String,OAuth2Authentication> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void store(String code, OAuth2Authentication authentication) {
        redisTemplate.opsForValue().set(key(code),authentication,120, TimeUnit.SECONDS);
    }

    public String key(String code){
        return prefix+code;
    }


    @Override
    protected OAuth2Authentication remove(String code) {
        OAuth2Authentication oAuth2Authentication=redisTemplate.opsForValue().get(key(code));
        if(oAuth2Authentication!=null){
            redisTemplate.delete(key(code));
        }
        return oAuth2Authentication;
    }
}

定义oauth2 登出过滤器

/**
 * @program: spring-security-demo
 * @description: oauth2登出过滤器
 * @author: chenzejie
 * @create: 2019-09-06 17:00
 **/
public class Oauth2LogoutFilter extends GenericFilterBean {

    private RequestMatcher requiresLogoutRequestMatcher;
    private String logoutSucessResponse;
    private TokenStore tokenStore;
    private EntityResponseRenderer entityResponseRenderer=new EntityResponseRenderer();

    /**
     * 默认构造参数需要接收一个请求匹配器,用于处理指定的登出请求
     * @param requiresLogoutRequestMatcher
     */
    public Oauth2LogoutFilter(RequestMatcher requiresLogoutRequestMatcher) {
        this.requiresLogoutRequestMatcher=requiresLogoutRequestMatcher;
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /**
         * 如果请求不符合登出请求则忽略
         */
        if(!requiresLogout((HttpServletRequest)request)){
            chain.doFilter(request, response);
            return;
        }

        /**
         * 因为当前过滤器是绑定在OAuth2AuthenticationProcessingFilter后面的, OAuth2AuthenticationProcessingFilter
         * 是负责校验token的合法性, 当到达这里时证明身份已经通过校验,则可以拿到上下文的身份信息
         */
        Authentication authentication=SecurityContextHolder.getContext().getAuthentication();
        if(authentication!=null){
            /**
             * OAuth2AuthenticationProcessingFilter 通过校验后会把token存放在request的作用域里
             * key为OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE
             */
            String access_token=(String)request.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE);
            //设置当前上下文的身份信息为null
            SecurityContextHolder.getContext().setAuthentication(null);
            //从token存储器中获取OAuth2AccessToken
            OAuth2AccessToken oAuth2AccessToken=tokenStore.readAccessToken(access_token);

            /**
             * 使用token存储器删除accessToken 和 refreshToken
             */
            if(oAuth2AccessToken!=null){
                tokenStore.removeAccessToken(oAuth2AccessToken);
            }
            if(oAuth2AccessToken.getRefreshToken()!=null){
                tokenStore.removeRefreshToken(oAuth2AccessToken.getRefreshToken());
            }
            /**
             * 成功退出后的处理, 里面暂时做了输出一窜成功登出的字符串, 你可以在里面
             * 扩展自己的用户退出逻辑
             */
            onSuccessLogout(request,response);
            return;
        }

    }

    private void onSuccessLogout(ServletRequest request, ServletResponse response) throws IOException {
        entityResponseRenderer.writeHttpEntityResponse(
                new MessageEntity(logoutSucessResponse, HttpStatus.OK.value()),
                HttpStatus.OK,
                request,
                response
        );
    }

    protected boolean requiresLogout(HttpServletRequest request) {
        return requiresLogoutRequestMatcher.matches(request);
    }

    public void setLogoutSucessResponse(String logoutSucessResponse) {
        this.logoutSucessResponse = logoutSucessResponse;
    }


    public void setTokenStore(TokenStore tokenStore) {
        this.tokenStore = tokenStore;
    }


}

/**
 * @program: spring-security-demo
 * @description: 登出过滤器的配置项,委派给HttpSecurity调用
 * @author: chenzejie
 * @create: 2019-09-06 17:23
 **/
public class Oauth2LogoutConfigurer extends
        SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    /**
     * 处理退出请求的地址
     */
    private String defaultLogoutUrl="/oauth2/logout";
    /**
     * 退出成功后的字符串信息
     */
    private String logoutSuccessResponse="{'code':1,'message':'成功退出'}";
    /**
     * token存储器
     */
    private TokenStore tokenStore;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        Oauth2LogoutFilter filter=new Oauth2LogoutFilter(new AntPathRequestMatcher(defaultLogoutUrl));
        filter.setTokenStore(tokenStore);
        filter.setLogoutSucessResponse(logoutSuccessResponse);
        //把Oauth2LogoutFilter作用在OAuth2AuthenticationProcessingFilter后
        http.addFilterAfter(filter, OAuth2AuthenticationProcessingFilter.class);
    }

    public Oauth2LogoutConfigurer defaultLogoutUrl(String defaultLogoutUrl) {
        this.defaultLogoutUrl = defaultLogoutUrl;
        return this;
    }

    public Oauth2LogoutConfigurer logoutSuccessResponse(String logoutSuccessResponse) {
        this.logoutSuccessResponse = logoutSuccessResponse;
        return this;
    }

    public Oauth2LogoutConfigurer tokenStore(TokenStore tokenStore) {
        this.tokenStore = tokenStore;
        return this;
    }
}

手机验证码认证相关代码

Redis操作手机验证码相关代码

/**
 * Redis操作手机验证码服务
 */
@Service
public class RedisCodeService {

    private final static String SMS_CODE_PREFIX = "SMS_CODE:";
    private final static Integer TIME_OUT = 300;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 保存验证码到 redis
     *
     * @param smsCode 短信验证码
     */
    public void save(String smsCode, String mobile) {
        redisTemplate.opsForValue().set(key( mobile), smsCode , TIME_OUT, TimeUnit.SECONDS);
    }

    /**
     * 获取验证码
     *
     * @return 验证码
     */
    public String get( String mobile)  {
        return redisTemplate.opsForValue().get(key( mobile));
    }

    /**
     * 移除验证码
     *
     */
    public void remove( String mobile) {
        redisTemplate.delete(key( mobile));
    }

    private String key( String mobile)  {
        return SMS_CODE_PREFIX + ":" + mobile;
    }
}


定义基于短信认证的异常

/**
 * @program: spring-security-demo
 * @description:  定义基于短信认证的异常
 * @author: chenzejie
 * @create: 2019-09-02 16:44
 **/
public class SmsAuthenicationException extends AuthenticationException {
    public SmsAuthenicationException(String msg, Throwable t) {
        super(msg, t);
    }

    public SmsAuthenicationException(String msg) {
        super(msg);
    }
}


短信验证码授权过滤器

/**
 * @program: spring-security-demo
 * @description:    短信验证码授权过滤器,用于给用户发送短信验证码
 * @author: chenzejie
 * @create: 2019-09-02 16:37
 **/
public class SmsAuthorizeFilter extends GenericFilterBean {
    private RedisCodeService redisCodeService;
    private RequestMatcher requiresAuthenticationRequestMatcher;
    private EntityResponseRenderer entityResponseRenderer=new EntityResponseRenderer();
    public SmsAuthorizeFilter() {
        processesUrl("/oauth/sms/authorize");
    }

    public RedisCodeService getRedisCodeService() {
        return redisCodeService;
    }

    public void setRedisCodeService(RedisCodeService redisCodeService) {
        this.redisCodeService = redisCodeService;
    }


    @Override
    public void afterPropertiesSet() throws ServletException {
        Assert.notNull(redisCodeService, "RedisCodeService must be specified");
    }

    public void processesUrl(String filterProcessesUrl) {
        this.requiresAuthenticationRequestMatcher=new AntPathRequestMatcher(filterProcessesUrl);
    }

    protected boolean requiresAuthentication(HttpServletRequest request) {
        return requiresAuthenticationRequestMatcher.matches(request);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;


        if (!requiresAuthentication((HttpServletRequest)request)) {
            chain.doFilter(request, response);
            return;
        }
        String mobile=request.getParameter("mobile");

        if(StringUtils.isEmpty(mobile)){
            entityResponseRenderer.writeHttpEntityResponse(
                    new MessageEntity("手机号不能为空",HttpStatus.UNAUTHORIZED.value()),
                    HttpStatus.UNAUTHORIZED,
                    request,
                    response
            );
           return;
        }

        String code=new Random().nextInt(999999)+"";
        redisCodeService.save(code,mobile);

        entityResponseRenderer.writeHttpEntityResponse(
                new MessageEntity("短信发送成功:"+code,HttpStatus.OK.value()),
                HttpStatus.OK,
                request,
                response
        );

    }
}


基于短信认证的token

/**
 * @program: spring-security-demo
 * @description:  基于短信认证的token
 * @author: chenzejie
 * @create: 2019-09-02 16:16
 **/
public class SmsAuthenicationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;
    private final Object credentials;
    public SmsAuthenicationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials=credentials;
        setAuthenticated(false);
    }

    public SmsAuthenicationToken(Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

    @Override
    public Object getCredentials() {
        return credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

基于短信的身份认证器

/**
 * @program: spring-security-demo
 * @description: 基于短信的身份认证器
 * @author: chenzejie
 * @create: 2019-09-02 16:14
 **/
public class SmsAuthenicationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailService;
    private RedisCodeService redisCodeService;

    public SmsAuthenicationProvider(UserDetailsService userDetailService) {
        this.userDetailService = userDetailService;
    }

    public RedisCodeService getRedisCodeService() {
        return redisCodeService;
    }

    public void setRedisCodeService(RedisCodeService redisCodeService) {
        this.redisCodeService = redisCodeService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenicationToken smsAuthenicationToken=(SmsAuthenicationToken)authentication;
        String mobile=smsAuthenicationToken.getPrincipal().toString();
        String redisCode=redisCodeService.get(mobile);
        String code=smsAuthenicationToken.getCredentials().toString();
        if(StringUtils.isEmpty(redisCode)) {
            throw new SmsAuthenicationException("请重新发送验证码");
        }

        if(!redisCode.equals(code)) {
            throw new SmsAuthenicationException("验证码错误");
        }


        UserDetails userDetail= userDetailService.loadUserByUsername(mobile);
        SmsAuthenicationToken result=new SmsAuthenicationToken(userDetail,null, userDetail.getAuthorities());
        //最后要删掉验证码
        redisCodeService.remove(mobile);
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        /**
         * 当 authentication的类型为SmsAuthenicationToken 会使用本类的认证器进行身份认证
         */
        return (SmsAuthenicationToken.class
                .isAssignableFrom(authentication));
    }
}


为springSecurity扩展基于短信的身份认证器的配置项

/**
 * @program: spring-security-demo
 * @description:
 * 为springSecurity扩展基于短信的身份认证器 SmsAuthenicationProvider,
 * 的配置项,委派给HttpSecurity调用
 * @author: chenzejie
 * @create: 2019-09-24 18:11
 **/
public class SmsAuthenticationConfigurerAdapter extends GlobalAuthenticationConfigurerAdapter {
    private final ApplicationContext context;

    public SmsAuthenticationConfigurerAdapter(ApplicationContext context) {
        this.context = context;
    }

    /**
     * 这里需要添加springSecurity默认的账号密码验证器 DaoAuthenticationProvider,
     * DaoAuthenticationProvider默认在 InitializeUserDetailsBeanManagerConfigurer 进行配置, 如果
     * 对springSecurity的身份验证器进行扩展则会失效。所以在这里我们需要配置一个
     * 默认的账号密码验证器 DaoAuthenticationProvider
     *
     * @param auth
     */
    private void buildDefaultUserDetailsManager(AuthenticationManagerBuilder auth){
        UserDetailsService userDetailsService = getBeanOrNull(
                UserDetailsService.class);

        if (userDetailsService == null) {
            return;
        }
        PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);

        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        if (passwordEncoder != null) {
            provider.setPasswordEncoder(passwordEncoder);
        }
        auth.authenticationProvider(provider);
    }


    @Override
    public void init(AuthenticationManagerBuilder auth) throws Exception {
        buildDefaultUserDetailsManager(auth);
        UserDetailsService userDetailsService = getBeanOrNull(
                UserDetailsService.class);
        if (userDetailsService == null) {
            return;
        }
        RedisCodeService redisCodeService=getBeanOrNull(
                RedisCodeService.class);
        SmsAuthenicationProvider smsAuthenicationProvider=new SmsAuthenicationProvider(userDetailsService);
        smsAuthenicationProvider.setRedisCodeService(redisCodeService);
        auth.authenticationProvider(smsAuthenicationProvider);

    }

    /**
     * @return
     */
    private <T> T getBeanOrNull(Class<T> type) {
        String[] userDetailsBeanNames = this.context
                .getBeanNamesForType(type);
        if (userDetailsBeanNames.length != 1) {
            return null;
        }
        return this.context
                .getBean(userDetailsBeanNames[0], type);
    }

}

扩展spring security的token获取方式


/**
 * @program: spring-security-demo
 * @description: 扩展spring security的token获取方式,原生的的token获取方式只支持 password\authorization_code 这些
 * 如果需要扩展短信验证码获取token需要继承AbstractTokenGranter 进行扩展
 * @author: chenzejie
 * @create: 2019-09-24 13:43
 **/
public class ResourceOwnerSmsTokenGranter extends AbstractTokenGranter {


    private static final String GRANT_TYPE = "sms";

    private final AuthenticationManager authenticationManager;

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

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

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
        String mobile = parameters.get("username");
        String validCode = parameters.get("code");
        // Protect from downstream leaks of password
        parameters.remove("code");
        /**
         * 构造要进行认证的AbstractAuthenticationToken , 这里我们创建SmsAuthenicationToken,这样就会委派给
         * SmsAuthenicationProvider进行认证
         */
        Authentication userAuth = new SmsAuthenicationToken(mobile,validCode);
        ((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: " + mobile);
        }

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



启动配置相关代码

application.properties

security.basic.enabled=false
spring.redis.host=10.0.0.77
spring.redis.port=6379
#Session集群处理   把session的存储方式修改为redis,实现session集群
spring.session.store-type=redis

Spring配置类入口

/**
 * 配置入口, 初始化相关的bean,并加载 AuthorizationServerConfig   ResourceServerConfig
 */
@Configuration
@Import({AuthorizationServerConfig.class,ResourceServerConfig.class})
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private UserDetailsService userDetailService;
    @Autowired
    private RedisCodeService redisCodeService;


    /**
     * 创建MyUserDetailService实例 并在IOC容器注册
     * @return
     */
    @Bean(name = BeanIds.USER_DETAILS_SERVICE)
    public UserDetailsService userDetailService(){
        this.userDetailService=new MyUserDetailService();
        return this.userDetailService;
    }

    /**
     * 为springSecurity扩展基于短信的身份认证器 SmsAuthenicationProvider的配置项
     * springSecurity在启动后会调用在IOC容器所有的GlobalAuthenticationConfigurerAdapter实现类的init方法,
     * 为spring security的身份认证进行扩展
     */
    @Bean
    public GlobalAuthenticationConfigurerAdapter smsAuthenticationConfigurerAdapter(
            ApplicationContext applicationContext,
            RedisCodeService redisCodeService
            ){
        return new SmsAuthenticationConfigurerAdapter(applicationContext);
    }


    /**
     * 创建RedisToken存储,并在IOC容器注册,默认spring oauth2使用的是InMemoryTokenStore, 不适用于微服务场景
     * @param redisConnectionFactory  依赖于RedisConnectionFactory 需要spring注入
     * @return
     */
    @Bean
    @Autowired
    public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory){
        RedisTokenStore redisTokenStore=new RedisTokenStore(redisConnectionFactory);
        return redisTokenStore;
    }

    /**
     * 密码加密器,并在IOC容器注册
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 创建基于redis的授权码存储服务,并在IOC容器注册
     * @return
     */
    @Autowired
    @Bean
    public InRedisAuthorizationCodeServices inRedisAuthorizationCodeServices(RedisTemplate redisTemplate){
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return new InRedisAuthorizationCodeServices(redisTemplate);
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 构造短信发送过滤器,并把该过滤器绑定到ExceptionTranslationFilter前面
         */
        SmsAuthorizeFilter smsAuthorizeFilter=new SmsAuthorizeFilter();
        smsAuthorizeFilter.setRedisCodeService(redisCodeService);

        String authorizeSmsPath="/oauth/sms/authorize";
        /**
         *  当前的权限拦截器组只拦截 /login /oauth/authorize /oauth/sms/authorize 3个请求
         *  /oauth/authorize: spring-security的获取授权码的接口地址,并跳转到第三方地址的接口地址,
         *  但是默认的spring-security-oauth2  的AuthorizationServerSecurityConfiguration
         *  构造的拦截器组没有对/oauth/authorize 进行拦截,导致若没进行身份认证, 访问该接口则会抛异常,所以在本过滤器组添加拦截
         *
         *  /oauth/sms/authorize: 获取手机验证码, 不需要进行身份认证
         *
         *  /login: 用户获取授权码时候,会进行身份校验, 若未登陆会跳转到/login 登录页 也需要显示声明该拦截地址
         *
         */
        http.requestMatchers()
                .antMatchers("/login","/oauth/authorize",authorizeSmsPath).and()
                .formLogin().and()
                .authorizeRequests()
                .antMatchers(authorizeSmsPath).permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                .addFilterAfter(smsAuthorizeFilter, ExceptionTranslationFilter.class)
        ;

    }

}

认证服务配置


/**
 * @program: spring-security-demo
 * @description:   认证服务配置
 * @author: chenzejie
 * @create: 2019-09-02 09:49
 **/
@EnableAuthorizationServer
public class AuthorizationServerConfig  extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private InRedisAuthorizationCodeServices inRedisAuthorizationCodeServices;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 自定义token授权器
     * spring-security默认的token生成器在AuthorizationServerEndpointsConfigurer.getDefaultTokenGranters()
     * 生成, 如果开发者想扩展自己的token生成器, 需要把原有的逻辑代码拷贝过来, 并追加自己的token生成器,本方法就是
     * 沿袭getDefaultTokenGranters()的代码逻辑, 并追加了ResourceOwnerSmsTokenGranter 短信token生成器
     *
     * @return
     */
    public TokenGranter customTokenGranter(ClientDetailsService clientDetails
    ,AuthorizationServerTokenServices tokenServices, AuthorizationCodeServices authorizationCodeServices,
    OAuth2RequestFactory requestFactory
    ){

        List<TokenGranter> defaultTokenGranters = new ArrayList<TokenGranter>();
        defaultTokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
                requestFactory));
        defaultTokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
        defaultTokenGranters.add(implicit);
        defaultTokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
        if (authenticationManager != null) {
            defaultTokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
                    clientDetails, requestFactory));

            defaultTokenGranters.add(new ResourceOwnerSmsTokenGranter(authenticationManager, tokenServices,
                    clientDetails, requestFactory));
        }

        TokenGranter tokenGranter=new CompositeTokenGranter(defaultTokenGranters);
        return tokenGranter;

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        /**
         *  对认证的token接口进行配置
         *  tokenStore(tokenStore): 配置基于redis的token存储器
         *  authorizationCodeServices(inRedisAuthorizationCodeServices):配置基于redis的授权码存储器
         *  authenticationManager(authenticationManager):配置身份认证服务,如果不配置则不支持基于密码的授权模式
         *  .tokenGranter(): 配置自定义token生成器
         *  .userDetailsService(userDetailsService):  主要作用于刷新token操作重新读取用户信息
         */
        endpoints
        .tokenStore(tokenStore)
        .authorizationCodeServices(inRedisAuthorizationCodeServices)
        .authenticationManager(authenticationManager)
        .userDetailsService(userDetailsService)
        .tokenGranter(
                customTokenGranter(endpoints.getClientDetailsService(),endpoints.getTokenServices(),
                        endpoints.getAuthorizationCodeServices(),endpoints.getOAuth2RequestFactory()
                        )
        )
        ;
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //如果AppId的秘钥是进行加密的,则需要添加为AuthorizationServerSecurityConfigurer配置密码加密器
        security.passwordEncoder(passwordEncoder);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /**
         * 配置appClient详情, 下面演示的是基于内存存储的, 不可在程序运行期间进行修改,不推荐使用
         * 建议使用clients.jdbc(), 把appClient存储在数据库里,你也可以扩展基于redis mongodb的appClient
         * 读取器, 需要实现ClientDetailsService
         *
         */
        clients.inMemory()
                //appId
                .withClient("test")
                //秘钥
                .secret(passwordEncoder.encode("test1234"))
                //token失效时间(秒)
                .accessTokenValiditySeconds(3600)
                //刷新token失效时间(秒)
                .refreshTokenValiditySeconds(864000)
                //作用域
                .scopes("all")
                //app注册跳转地址
                .redirectUris("http://www.baidu.com")
                //支持的token生成模式, 这里把常见的密码模式、授权码模式、刷新token模式、短信认证模式都加上了
                .authorizedGrantTypes("password","authorization_code","refresh_token","sms")
                ;
    }

}


资源服务配置


/**
 * @program: spring-security-demo
 * @description: 资源服务配置, 主要作用是做登出的处理,因为登出需要依赖于token校验,
 * token校验的过滤器 OAuth2AuthenticationProcessingFilter 在ResourceServerConfiguration进行配置
 * 所以需要加载资源服务的配置项
 * @author: chenzejie
 * @create: 2019-09-02 10:44
 **/
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {

    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        //把OAuth2登出配置类委派给HttpSecurity调用
        http.apply(new Oauth2LogoutConfigurer()).tokenStore(tokenStore);
    }
}

测试基于密码模式获取token

使用postman发送POST请求localhost:8080/oauth/token:

image

grant_type填password,表示密码模式,然后填写用户名和密码,除了这几个参数外,
我们还需要在请求头中填写:

image

key为Authorization,value为Basic加上client_id:client_secret经过base64加密后的值: Basic dGVzdDp0ZXN0MTIzNA==


你可以在下面提供的在线加密链接进行加密测试
http://tool.chinaz.com/Tools/Base64.aspx):

image

参数填写无误后,点击发送便可以获取到令牌Token:


{
    "access_token": "b52ab19a-dc6c-453a-900f-9563c7f5ce3b",
    "token_type": "bearer",
    "refresh_token": "92423cbc-75e0-415d-be94-af1f7ee032de",
    "expires_in": 3599,
    "scope": "all"
}

测试基于短信模式获取token

1、使用postman发送获取短信请求:
localhost:8080/oauth/sms/authorize?mobile=13242404681

image


2、使用postman发送POST请求:localhost:8080/oauth/token

image
grant_type填sms,表示短信验证码模式,code填收到的短信,头部也需要填写Authorization信息,内容和密码模式介绍的一致,这里就不截图了。点击发送,也可以获得令牌:


{
    "access_token": "b52ab19a-dc6c-453a-900f-9563c7f5ce3b",
    "token_type": "bearer",
    "refresh_token": "92423cbc-75e0-415d-be94-af1f7ee032de",
    "expires_in": 3599,
    "scope": "all"
}

测试基于授权码模式获取token

接下来开始往认证服务器请求授权码。打开浏览器,访问

http://localhost:8080/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://www.baidu.com&scope=all&state=hello

URL中的几个参数,response_type必须为code,表示授权码模式,client_id就是刚刚在配置文件中手动指定的test,redirect_uri这里为客户端配置的跳转地址,主要是用来重定向获取授权码的,scope指定为all,表示所有权限。

访问这个链接后,页面如下所示:

image
如果要自定义登陆页面, 在SecurityConfig里为HttpSecurity配置formLogin的loginPage方法配置

需要登录认证,根据我们前面定义的UserDetailService逻辑,这里用户名随便输,密码为123456即可。输入后,页面跳转如下所示:

image

选择同意Approve,然后点击Authorize按钮后,页面跳转到了我们指定的redirect_uri,并且带上了授权码信息:

image

到这里我们就可以用这个授权码从认证服务器获取令牌Token了。

使用postman发送如下请求POST请求localhost:8080/oauth/token:

image
grant_type固定填authorization_code,code为上一步获取到的授权码,client_id和redirect_uri、scope必须和我们上面定义的一致。 头部也需要填写Authorization信息,内容和密码模式介绍的一致,这里就不截图了。点击发送,也可以获得令牌:

{
    "access_token": "60d373b3-3ad1-46d7-99fd-1501d5cac572",
    "token_type": "bearer",
    "refresh_token": "c9688f78-836d-4b32-9a64-a0890c87c81e",
    "expires_in": 3599,
    "scope": "all"
}

自定义授权页

在这里我们看到登陆页面和授权页面很简陋, 不符合线上的需求,自定义页面的配置很简单,这里就不概述了,在这里介绍下如何自定义授权页面,在上面的代码基础下, 我们增加如下配置:

添加模板依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

配置模板文件
在application.properties追下如下内容

spring.thymeleaf.prefix=classpath:/views/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false

在resources/views 新建base-grant.html 授权页面文件

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>授权</title>
</head>

<style>

    html{
        padding: 0px;
        margin: 0px;
    }

    .title {
        background-color: #E9686B;
        height: 50px;
        padding-left: 20%;
        padding-right: 20%;
        color: white;
        line-height: 50px;
        font-size: 18px;
    }
    .title-left{
        float: right;
    }
    .title-right{
        float: left;
    }
    .title-left a{
        color: white;
    }
    .container{
        clear: both;
        text-align: center;
    }
    .btn {
        width: 350px;
        height: 35px;
        line-height: 35px;
        cursor: pointer;
        margin-top: 20px;
        border-radius: 3px;
        background-color: #E9686B;
        color: white;
        border: none;
        font-size: 15px;
    }
</style>
<body style="margin: 0px">
<div class="title">
    <div class="title-right">OAUTH-BOOT 授权</div>
    <div class="title-left">
        <a href="#help">帮助</a>
    </div>
</div>
<div class="container">
    <h3 th:text="${clientId}+' 请求授权,该应用将获取你的以下信息'"></h3>
    <p>昵称,头像和性别</p>
    授权后表明你已同意 <a  href="#boot" style="color: #E9686B">OAUTH-BOOT 服务协议</a>
    <form method="post" action="/oauth/authorize">

        <input type="hidden" name="user_oauth_approval" value="true"/>

        <div th:each="item:${scopes}">
            <input type="radio" th:name="'scope.'+${item}" value="true" hidden="hidden" checked="checked"/>
        </div>

        <button class="btn" type="submit"> 同意/授权</button>

    </form>
</div>
</body>
</html>

定义界面Controller

spring-security内部跳转授权页面的地址为/oauth/confirm_access,所以你的接口地址定义为/oauth/confirm_access即可覆盖spring-security默认的授权页面

@Controller
@SessionAttributes("authorizationRequest")
public class BootGrantController {
 
    @RequestMapping("/oauth/confirm_access")
    public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
 
        AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
        ModelAndView view = new ModelAndView();
        view.setViewName("base-grant");
        view.addObject("clientId", authorizationRequest.getClientId());
        view.addObject("scopes",authorizationRequest.getScope());
        return view;
    }
 
}

效果图
image


测试刷新token

我们回顾前面获取到的token内容:


{
    "access_token": "54438695-8c98-4553-ab71-764e8b8ccdbe",
    "token_type": "bearer",
    "refresh_token": "20ca6925-76d8-405a-b67e-00c9534a28c8",
    "expires_in": 3521,
    "scope": "all"
}

其中access_token是我们访问资源服务器所需要携带的令牌,但是这个令牌会有过期时间,过期时间为expires_in字段信息,默认为1小时,当过期后我们需要重新获取新的token,除了前面说的几种获取token方式外,还有一种是根据刷新令牌获取新的token,实例如下:

使用postman发送如下请求POST请求localhost:8080/oauth/token:

image

image grant_type固定填refresh_token,refresh_token 为获取到的刷新token。 头部也需要填写Authorization信息,内容和密码模式介绍的一致,这里就不截图了。点击发送,也可以获得令牌:

{
    "access_token": "188d997c-8c0e-4a70-adf7-3d69c06d1645",
    "token_type": "bearer",
    "refresh_token": "20ca6925-76d8-405a-b67e-00c9534a28c8",
    "expires_in": 3599,
    "scope": "all"
}
到这里,认证服务器的相关知识都讲完了, 接下来我们看下一篇资源服务器的配置,如何用token访问受保护的资源
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值