spring boot + spring security+ oauth2鉴权双系统开发记录

 以前项目在授权的时候,都是使用的自定义的授权令牌token,具体的实现思路是在登录的时候生成一个随机的token,设置超时时间后放入redis缓存中,前端每次访问需要鉴权的资源的时候都需要附带上token令牌,后端写一个切面对访问路径进行验证,如果需要鉴权的就进行token令牌对比,通过后刷新令牌的超时时间,当前端token失效了,则返回给前端失效结果让前端重新登录。

 后来又使用过shiro做过鉴权,shiro鉴权比自定义的功能强大且提供了一些操作权限及前端标签验证,使用起来更加方便,但是最近项目开发过程中,发现shiro已经无法满足项目需求了。(ps:原先项目分了两个端,一个是后台管理端接口及app接口端,两端使用的分别是shiro鉴权和自定义网关鉴权,需要将两个项目合成一个项目,因为是两个登录方式,且项目要求要使用spring security + oauth 2鉴权方式;吐槽点:明明一个自定义的鉴权完全可以实现的,可是技术总监就是要求要这样子做,作为开发的我们也是没有办法呀!谁让我们没人家级别高呢!汗~~~

 虽然使用自定义的授权方式也可以实现我们的需求,但是本着能使用开源的并且更加安全高效的原则来说,研究了一下最常见的授权方式 oauth2,并结合查看了一下spring security相结合的使用方式。

如果有想要学习的同学,可以去GitHub的这个网址查看,写的很详细:https://github.com/lexburner/oauth2-demo

 

好了,废话不多说,直接贴代码:

鉴权授权服务器,项目是spring boot 2.0以上版本,如果是其它的版本的话,查看上面的网址进行配置。

package com.jishi.api.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
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.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
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.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 * description:auth2鉴权授权服务器
 *
 * @author sp
 * @date 2018/7/19
 */
@Configuration
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {

  private static final String RESOURCE_API_ID = "api";

  /**
   * auth 2 资源服务器
   */
  @Configuration
  @EnableResourceServer
  protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
      resources.resourceId(RESOURCE_API_ID).stateless(true);
//			resources.resourceId(RESOURCE_WEB_ID).stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
      http

          //设置不需要鉴权的目录
          .authorizeRequests()
          //swagger
          .antMatchers("/swagger-ui.html").permitAll()
          .antMatchers("/webjars/**").permitAll()
          .antMatchers("/swagger-resources").permitAll()
          .antMatchers("/swagger-resources/**").permitAll()
          .antMatchers("/v2/api-docs").permitAll()
          //回调
          .antMatchers("/callback/**").permitAll()
          //cms
          .antMatchers("/sys/user/reg").permitAll()
          .antMatchers("/sys/user/sendMailCode").permitAll()
          .antMatchers("/sys/user/checkMailCode").permitAll()
          .antMatchers("/sys/user/forgetPassword").permitAll()
          .antMatchers("/sys/user/login").permitAll()
          //api
          .antMatchers("/user/login/getVertifyCode/**").permitAll()//验证码接口
          .antMatchers("/user/login/loginOrRegister/**").permitAll()//登录接口
          .antMatchers("/creidt/protocol/**").permitAll()
          .antMatchers("/user/banner/**").permitAll()//登录页合同信息
          //除了以上资源,其它资源都需要鉴权才能访问
          .antMatchers("/**").authenticated();

    }
  }

  /**
   * 授权服务器
   */
  @Configuration
  @EnableAuthorizationServer
  protected static class AuthorizationServerConfiguration extends
      AuthorizationServerConfigurerAdapter {

    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private UserDetailsService myUserDetailService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

//        password 方案一:明文存储,用于测试,不能用于生产
        String finalSecret = "123456";
//        password 方案二:用 BCrypt 对密码编码
//        String finalSecret = new BCryptPasswordEncoder().encode("123456");
      // password 方案三:支持多种编码,通过密码的前缀区分编码方式
//      String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode("123456");
      //配置两个客户端,一个用于password认证一个用于client认证
      clients.inMemory().withClient("client_1")
          .resourceIds(RESOURCE_API_ID)
          .authorizedGrantTypes("password", "refresh_token")
          .scopes("select")
          .authorities("oauth2")
          .secret(finalSecret);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
      endpoints
          .tokenStore(new RedisTokenStore(redisConnectionFactory))
          .authenticationManager(authenticationManager)
          .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
          .userDetailsService(myUserDetailService);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
      //允许表单认证
      oauthServer.allowFormAuthenticationForClients();
    }
  }
}

 spring security的资源服务器

package com.jishi.api.config;

import com.google.common.collect.ImmutableList;
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.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

/**
 * description:
 *
 * @author sp
 * @date 2018/7/19
 */
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Autowired
  private UserDetailsService myUserDetailService;

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

  @Override
  public UserDetailsService userDetailsServiceBean() throws Exception {
    return myUserDetailService;
  }

  @Override
  protected UserDetailsService userDetailsService() {
    return myUserDetailService;
  }

  /**
   * springboot2.0 删除了原来的 plainTextPasswordEncoder https://docs.spring.io/spring-security/site/docs/5.0.4.RELEASE/reference/htmlsingle/#10.3.2
   * DelegatingPasswordEncoder
   */

  // password 方案一:明文存储,用于测试,不能用于生产
  @Bean
  @SuppressWarnings("all")
  PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }

// password 方案二:用 BCrypt 对密码编码
//    @Bean
//    PasswordEncoder passwordEncoder(){
//        return new BCryptPasswordEncoder();
//    }
// password 方案三:支持多种编码,通过密码的前缀区分编码方式,推荐
//	@Bean
//	PasswordEncoder passwordEncoder(){
//		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
//	}

  /**
   * 这一步的配置是必不可少的,否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
   */
  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    AuthenticationManager manager = super.authenticationManagerBean();
    return manager;
  }

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

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // @formatter:off
    http.csrf().disable()
        .requestMatchers().anyRequest()
        .and()
        .authorizeRequests()
        .antMatchers( "/api/open/**", "/","/favicon.ico").permitAll();
    // @formatter:on
  }

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    final CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(ImmutableList.of("*"));
    configuration.setAllowedMethods(ImmutableList.of("HEAD",
        "GET", "POST", "PUT", "DELETE", "PATCH"));
    // setAllowCredentials(true) is important, otherwise:
    // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
    configuration.setAllowCredentials(true);
    // setAllowedHeaders is important! Without it, OPTIONS preflight request
    // will fail with 403 Invalid CORS request
    configuration.setAllowedHeaders(ImmutableList.of("*"));
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
  }
}

自定义的userDetailService

package com.jishi.api.service.auth;

import com.jishi.api.config.User;
import com.jishi.api.mapper.CmsSysUserMapper;
import com.jishi.commons.constants.RedisKey;
import com.jishi.commons.entity.CmsSysUser;
import com.jishi.commons.utils.DesUtil;
import javax.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import redis.clients.jedis.JedisCluster;

/**
 * description:auth鉴权service
 *
 * @author sp
 * @date 2018/7/19
 */
@Service("myUserDetailService")
public class MyUserDetailService implements UserDetailsService {

  @Resource
  private CmsSysUserMapper cmsSysUserMapper;
  @Autowired
  private JedisCluster jedisCluster;
  @Override
  public UserDetails loadUserByUsername(String mobileOrUsername) throws UsernameNotFoundException {

    try {
      //cms
      if(StringUtils.equals(mobileOrUsername.split("_")[0],"cms")){
        CmsSysUser cmsSysUser = new CmsSysUser();
        cmsSysUser.setUsername(mobileOrUsername.split("_")[1]);
        cmsSysUser.setEmail(mobileOrUsername.split("_")[1]);
        CmsSysUser query = cmsSysUserMapper.getUserByName(cmsSysUser);
        if(query != null){
          //有数据,返回校验
          User user = new User();
          user.setUsername(mobileOrUsername);
          user.setPassword(DesUtil.decryptFinalKey(query.getPassword()));
          return user;
        }else {
          throw new UsernameNotFoundException("can't find user by username" + mobileOrUsername);
        }
      }

      //app
      String redisCode = jedisCluster.get(RedisKey.VERTIFYCODE.toString() + mobileOrUsername.split("_")[1]);
      User user = new User();
      user.setPassword(redisCode);
      user.setUsername(mobileOrUsername);
      return user;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }


}

ps:自定义的userDetailService里面并没有任何比较,此处说明一下,比较是框架自行进行的,这里需要做的是返回正确的对比数据,也就是username跟password,因为我的项目是原先的两个项目结合的,一个是后台管理cmis的密码登录,另外一个是app验证码方式登录,所以这里app端的密码设置的是验证码作为密码。

具体的使用流程为:

   1:cmis端,访问无需鉴权的登录接口进行登录,当登录通过后,访问http://localhost:10100/oauth/token?username=cms_admin&password=123456&grant_type=password&scope=select&client_id=client_1&client_secret=123456

获取access_token,后面需要鉴权的接口都附带上access_token即可。

  2 : app端,访问app的验证码登录,通过后,访问http://localhost:10100/oauth/token?username=api_admin&password=123456&grant_type=password&scope=select&client_id=client_1&client_secret=123456

获取access_token。

注:cmis端的地址中的password是登录密码,app端的password是手机获取的验证码,框架使用的是redis作为缓存,使用的默认的端口,所以配置的时候本地需要启动一个默认配置的redis,否则会报错,后续redis的配置可以自己配置一下,或者等我后续更新。

你可能感兴趣的:(Java开发笔记)