微服务安全-基于SpringCloud-OAuth2搭建微服务开放平台 jwt生成与校验

源码:TokenEndpoint

在org.springframework.security.oauth2.provider.endpoint包下
1.获取登录对象信息
 SecurityContextHolder.getContext().getAuthentication().getPrincipal();

2.从返回的token信息获取ip地址(additionalInformation里面是自定义的信息,ipAddress也是自定义的)
 String ipAddress = accessToken.
                        getAdditionalInformation().get("ipAddress").toString();


3.退出登录
  //自己封装 实际调用tokenStore.removeAccessToken(existingAccessToken);
  String authorization = request.getHeader("Authorization");
  if (authorization != null && authorization.contains("Bearer")) {
      String tokenId = authorization.substring("Bearer".length() + 1);
      tokenServices.revokeToken(tokenId);
  }

//源码里面  existingAccessToken的获取见下面两个方法
if (accessToken.getRefreshToken() != null) {
	tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(existingAccessToken);

//根据tokenValue获取accessToken
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
//根据authentication获取accessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);

4.获取request和request的Ip 
   HttpServletRequest request = ((ServletRequestAttributes) Objects
                        .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
                String ipAddress = existingAccessToken.
                        getAdditionalInformation().get(AuthConstant.TOKEN_IP_ADDRESS_KEY).toString();

一。搭建微服务开放平台的目的与达到的效果

一。搭建微服务开放平台的目的与达到的效果
 目的:管理微服务中的开放接口,对接口进行授权认证
 达到的效果:配置一下appId和appSecret,就自动有如下功能
 
a.获取授权码:
   http://localhost:8080/oauth/authorize? client_id=c1&response_ype=code&scope=all&redirect_uri=http://www.baidu.com

b.获取accessToken     
   http://localhost:8080/oauth/token?   //通过授权码token
        grant_type=authorization_code  client_id=c1  client_secret=secret  code=5PgfcD   redirect_uri=http://www.baidu.com     
 
  http://localhost:8080/oauth/token?    //通过密码模式获取token(密码模式拿token不要先获取授权码)
        grant_type=password   client_id=c1   client_secret=secret username="张三”  password="123456"  scope="all"

c.刷新token
    http://localhost:8080/oauth/toekn?
       grant_type=refresh_token  refresh_token="454df"   client_id  client_secret

d.验证token是否有效       
   http://localhost:8080/oauth/check_token?
       token="sdfw555sd45d4f"
       

二。搭建授权服务中心 (类似微信平台 星云授权项目)

自己搭建授权服务地址

1.导入pom依赖  pom依赖见githua地址:https://gitee.com/four_hundred_and_twentyone/spring-security-oauth.git
	      <!-- spring-cloud-starter-oauth2 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>


2..两个配置类java文件 里面重写多个configure方法   配置了哪些客户秘钥/哪些资源服务可以访问本授权服务,token有效期等信息
   一个AuthorizationServerConfig继承AuthorizationServerConfigurerAdapter
   一个WebSecurityConfig        继承WebSecurityConfigurerAdapter

3.主程序启动测试 获取授权码  获取accessToken等功能     

三。搭建资源服务中心 (类似一个app应用 星云业务项目)

自己搭建资源服务

//这种方式是在微服务进行认证拦截,也可以统一在网关处拦截

1.导入pom依赖  和资源服务一致
	      <!-- spring-cloud-starter-oauth2 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>


2.主要一个类OAuth2ServerConfig 继承 ResourceServerConfigurerAdapter   类似加注解
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(this.resourceId)    //资源id
    }
    
    public void configure(HttpSecurity http){
        http.authorizeRequests().antMatchers(antMather).authenticated()  //拦截antMatcher请求
    }  
}
            	
3.resouces下配置文件配置授权服务中心地址
      security:
          oauth2:
            client:
               client-id: web                                           //appId
               client-secret: nebulait                                  //appSecret
            resource:          
               token-info-uri: http://localhost:8589/oauth/check_token  //从认证授权中心上验证token


4.主程序启动访问测试:http://localhost:8080/api/order/test
  Headers要加 Authorization : Bearer b2011201-e7cb-4977-a418-768dd5b43a53
  或者请求路径后面加上参数 http://localhost:8080/api/order/test?token=b2011201-e7cb-4977-a418-768dd5b43a53

在这里插入图片描述

四。代码案例

1、授权服务器配置

一个WebSecurityConfig 继承 WebSecurityConfigurerAdapter
AuthorizationServerConfig 继承 AuthorizationServerConfigurerAdapter

//WebSecurityConfig
@Component
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    //拦截所有请求,使用http-basic方式登录
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();
    }
}
AuthorizationServerConfig 
//简化版
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    //1.加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //2.用户信息  正常从数据库读取
    @Bean
    UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("user_1").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build() );
        userDetailsService.createUser(User.withUsername("user_2").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build());
        return userDetailsService;
    }

    //3.整合加密方式和用户信息
    @Bean
    AuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService());
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        return daoAuthenticationProvider;
    }

    //4.啥东东
    @Bean
    AuthenticationManager authenticationManager() {
        AuthenticationManager authenticationManager = new AuthenticationManager() {
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                return daoAuthenticationProvider().authenticate(authentication);
            }
        };
        return authenticationManager;
    }


    //1.配置appId secret  回到地址  token有效期
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
            .inMemory()
                .withClient("client_1")
                    .secret(passwordEncoder().encode("secret_1"))
                    .redirectUris("https://www.baidu.com/")
                    .authorizedGrantTypes("password", "refresh_token", "authorization_code")
                    .scopes("all")
                    .resourceIds("resourceId1")
                    .accessTokenValiditySeconds(36000)
                    .refreshTokenValiditySeconds(36000)

                .and()

                .withClient("client_2")
                .secret(passwordEncoder().encode("secret_2"))
                .redirectUris("https://www.baidu.com/")
                //授权码模式、密码模式、简单模式、客户端模式、regresh_token获取token
                .authorizedGrantTypes("authorization_code","password","implicit","client_credentials" "refresh_token",)
                .scopes("all")
                .resourceIds("resourceId2")
                .accessTokenValiditySeconds(36000)
                .refreshTokenValiditySeconds(36000)
        ;
    }

    //2.配置token类型
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
            .authenticationManager(authenticationManager())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
                .userDetailsService(userDetailsService())
        ;
    }

    //3.配置权限
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
        oauthServer
                .allowFormAuthenticationForClients()    //允许表单认证
                .checkTokenAccess("permitAll()");       //验证token
    }

//星云版
@Configuration
@EnableAuthorizationServer  //开启授权认证服务中心      在资源中心开启资源服务中心 @EnableResourceServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

	@Override  //配置客户端
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	    clients
	            .inMemory()                                                                 //暂时使用内存模式      
	            .withClient("web")                                                         //appId
	            .secret(passwordEncoder().encode("nebulait"))                              //客户端秘钥
	            .resourceIds(OPERATION_RESOURCE_ID, TIMED_TASK_RESOURCE_ID)                //可以访问的资源列表
	            .authorizedGrantTypes("password", "refresh_token""authorization_code")   //允许的授权类型
	            .scopes("all")                                                            //授权范围(客户端权限)	            
	            .accessTokenValiditySeconds(36000);                                      //Token 的有效期 10小时
	            .redirectUris("www.baidu.com")                                           //回调地址	
	}
	
	@Override  //配置令牌 的访问端点和服务
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
	    DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
	    defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());
	    endpoints
	            .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)  //允许Post,get提交
	            .tokenStore(tokenStore())
	            .authenticationManager(authenticationManager)              //autorizationCodeServive 授权码模式用这个
	            .userDetailsService(userDetailService)                     //密码模式需要 
	            .accessTokenConverter(defaultAccessTokenConverter)
	            .tokenEnhancer(new CustomTokenEnhancer())
	            .tokenServices(tokenServices())                           //令牌管理服务
            .exceptionTranslator(new CustomWebResponseExceptionTranslator());
	}
	
	@Override  //配置令牌的端点约束
	public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
	    oauthServer.allowFormAuthenticationForClients()
	            .tokenKeyAccess("permitAll()")
	            .checkTokenAccess("permitAll()")
	            .checkTokenAccess("isAuthenticated()");   //允许表单认证
	}
}


 授权服务器配置文件中 配置客户端时有多个appId  上面的第一个配置客户端改版
 @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient("wxmini")
                .resourceIds(AUTHORIZATION_RESOURCE_ID, WXMINI_RESOURCE_ID, PAYMENT_RESOURCE_ID, ORDER_RESOURCE_ID)
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("all")
                .secret(passwordEncoder().encode("nebulait"))
                .accessTokenValiditySeconds(accessTokenValiditySeconds)//一月失效
                .and()
                .withClient("app")
                .resourceIds(AUTHORIZATION_RESOURCE_ID, APP_RESOURCE_ID, PAYMENT_RESOURCE_ID, ORDER_RESOURCE_ID)
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("all")
                .secret(passwordEncoder().encode("nebulait"))
                .accessTokenValiditySeconds(accessTokenValiditySeconds)
                .and()
                .withClient("drainage")
                .resourceIds(AUTHORIZATION_RESOURCE_ID, ORDER_RESOURCE_ID)
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("all")
                .secret(passwordEncoder().encode("nebulait"))
                .redirectUris()
                .accessTokenValiditySeconds(-1);//永不失效
    }
  
  绑定数据库:clients.jdbc(dataSource);
2.资源服务器配置

一个文件ResourceServerConfig 继承 ResourceServerConfigurerAdapter

//简化版
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("resourceId1");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/api/**").authenticated();
    }
}

//星云版
@Configuration
@EnableResourceServer
public class ResourceServerConfi extends ResourceServerConfigurerAdapter {
    @Value("${nebula.security.resourceId}")
    private String resourceId;
    @Value("${nebula.security.antMather}")
    private String antMather;
    @Value("${nebula.security.noMather}")
    private String noMather;
    private final DefaultWebSecurityExpressionHandler expressionHandler;

    @Inject
    public OAuth2ServerConfig(DefaultWebSecurityExpressionHandler expressionHandler) {
        this.expressionHandler = expressionHandler;
    }

    public void configure(ResourceServerSecurityConfigurer resources) {
        resources
        	.resourceId(this.resourceId)    //资源id
        	.stateless(true)
        	.authenticationEntryPoint(new AuthExceptionEntryPoint())
        	.accessDeniedHandler(new CustomAccessDeniedHandler())
        	.expressionHandler(this.expressionHandler);
    }

    public void configure(HttpSecurity http) throws Exception {
        String[] urls = null;
        if (!StringUtils.isEmpty(this.noMather)) {
            urls = this.noMather.split(",");
        }

        if (urls != null) {
            ((AuthorizedUrl)((AuthorizedUrl)http.authorizeRequests()
            	.antMatchers(urls)).permitAll()                               //usrls是nomathers里面的内容,都放行
            	.antMatchers(new String[]{this.antMather})).authenticated()  //antMather里面的要认证
            	.accessDecisionManager(this.accessDecisionManager());
        } else {
            ((AuthorizedUrl)http.authorizeRequests()
            	.antMatchers(new String[]{this.antMather})).authenticated()  //拦截antMatcher请求
            	.accessDecisionManager(this.accessDecisionManager());
        }

    }

    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters = new ArrayList();
        decisionVoters.add(new WebExpressionVoter());
        decisionVoters.add(new URLBasedVoter());
        return new UnanimousBased(decisionVoters);
    }
}

五。完善

如上就可以完成测试获取授权码  获取令牌  刷新令牌  校验令牌  资源服务访问认证的功能,下面是做一些完善配置

资源需要哪些权限:WebSecurityConfig
   http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();

   http.csrf().disable().authorizeRequests()
            .antMatchers("/r/r1").hasAuthority("p1")
            .antMatchers("/r/r2").hasAuthority("p2")
            .antMatchers("/r/**").authenticated()   //所有的 /r开头请求必须认证通过
            .anyRequest().permitAll();             //除了 /r开头请求 其他可以访问

		
用户拥有哪些权限:AuthorizationServerConfig
    userDetailsService.createUser(User
        .withUsername("user_1")
        .password(passwordEncoder().encode("123456"))
        .authorities("ROLE_USER").build() 
    );

完善1:独立一个userDetailsService文件

(主要是实现UserDetailsService接口重写loadUserByName方法 ) 用户还没有读取数据库

//步骤1.新建一个类
@Component
public class MyUserDetailsService implements UserDetailsService {    
    @Bean
    private PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User.withUsername("myuser").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build();
        return null;
    }
}

//步骤2.把这个类注入到AuthorizationServerConfig使用,去掉之前的userDetailService
 @Autowired
 private MyUserDetailsService myUserDetailsService;

 原先userDetailsService() 内容如下
    @Bean
    UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("user_1").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build() );
        userDetailsService.createUser(User.withUsername("user_2").password(passwordEncoder().encode("123456")).authorities("ROLE_USER").build());
        return userDetailsService;
    }

//步骤3:AuthorizationServerConfig配置类的第二个configure,换成新注入的myUserDetailsService
   .userDetailsService(myUserDetailsService) 

完善2 用户的账号密码查询数据库

步骤1:pom添加加数据库驱动依赖
步骤2:yml添加数据库连接信息    这两步参考springBoot项目搭建里面的整合mybatis
步骤3:写一个dao通过userName查询用户信息
上面都是做数据库查询处理
步骤4: 修改MyUserDetailsService文件的loadUserByUsername方法
         根据用户名查询数据库,没有返回null;有返回数据库的用户名和密码
      @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        OrgOperationUser orgOperationUser = this.selectUserByName(userName);
        if (orgOperationUser == null){
            return null;
        }
        //UserDetails userDetails = User.withUsername("myuser").password(passwordEncoder().encode("123")).authorities("ROLE_USER").build();
        UserDetails userDetails = User.withUsername(orgOperationUser.getOou_name()).password(passwordEncoder().encode(orgOperationUser.getOou_password())).authorities("ROLE_USER").build();
        return userDetails;
    }

本来是如下写死的
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
      UserDetails userDetails = User.withUsername("myuser").password(passwordEncoder().encode("123")).authorities("ROLE_USER").build();
      return userDetails;
}

完善3 权限查询数据库

权限的硬编码写在MyUserDetailsService
  return User.withUsername(name).password(password().authorities("p1""p2").build();

思路:数据库查询用户权限,得到权限集合,然后转为数组array,上面.authorities("p1""p2")改为.authorities(array)
 //用户权限
List<String> userpermissionList = this.selectUserPermissions(orgOperationUser.getOou_id());
 //用户权限集合转数组
 String[] array = new String[userpermissionList.size()];
 userpermissionList.toArray(array);
   return User.withUsername(orgOperationUser.getOou_name()).password(orgOperationUser.getOou_password()).authorities(array).build();

资源服务获取权限
  SecurityContextHolder.getContext().getAuthentication().getAuthorities();
资源服务获取用户信息
  SecurityContextHolder.getContext().getAuthentication().getPrincipal();

完善4 MyUserDetailsService 用户返回的信息自定义

步骤一核心点:返回的用户信息MyUserDetails 实现 UserDetails 接口

本来:
      UserDetails userDetails = User.withUsername(orgOperationUser.getOou_name()).password(orgOperationUser.getOou_password()).authorities(array).build();       
      return userDetails;

改造后:
   MyUserDetails myUserDetails = this.buildMyUserDetails(orgOperationUser);
   return myUserDetails;
   //MyUserDetails 实现 UserDetails 接口

 private MyUserDetails buildMyUserDetails(OrgOperationUser orgOperationUser) {
        MyUserDetails myUserDetails = new MyUserDetails();
        myUserDetails.setId(orgOperationUser.getOou_id());
        myUserDetails.setCarrierId(orgOperationUser.getOou_carrier_id());
        myUserDetails.setDeptId(orgOperationUser.getOou_dept_id());
        myUserDetails.setCode(orgOperationUser.getOou_code());
        myUserDetails.setUsername(orgOperationUser.getOou_name());
        myUserDetails.setPassword(orgOperationUser.getOou_password());
        return myUserDetails;
    }

如上都在MyUserDetailsService 文件里

步骤二:写一个类CustomUserAuthenticationConverter继承DefaultUserAuthenticationConverter

public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
    @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        Map<String, Object> response = new LinkedHashMap<>();
        MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
        response.put(USERNAME, userDetails);
        if (!CollectionUtils.isEmpty(authentication.getAuthorities())) {
            response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        }
        return response;
    }
}

步骤三:在配置文件AuthorizationServerConfig的第二个configure加上如下两个代码

//2.配置token类型
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
    defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());

完善5 SecurityContextHolder.getContext().getAuthentication().getAuthorities()有用户权限

1.数据库查询权限(这边先角色)  角色集合遍历封装成 List<GrantedAuthority>类型

 private List<GrantedAuthority> selectUserPermissions(String userId) {
      List<GrantedAuthority> authorities = new ArrayList<>();
      //用户角色
      List<String> roleList = orgOperationUserMapper.userPermissions(userId);
      for (String role:roleList){
          SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role);
          authorities.add(simpleGrantedAuthority);
      }
      return authorities;
  }

2.buildMyUserDetails 设置这个集合
    private MyUserDetails buildMyUserDetails(OrgOperationUser orgOperationUser) {
        MyUserDetails myUserDetails = new MyUserDetails();
        myUserDetails.setId(orgOperationUser.getOou_id());
        myUserDetails.setCarrierId(orgOperationUser.getOou_carrier_id());
        myUserDetails.setDeptId(orgOperationUser.getOou_dept_id());
        myUserDetails.setCode(orgOperationUser.getOou_code());
        myUserDetails.setUsername(orgOperationUser.getOou_name());
        myUserDetails.setPassword(orgOperationUser.getOou_password());
        if (!CollectionUtils.isEmpty(selectUserPermissions(orgOperationUser.getOou_id()))){
            myUserDetails.setAuthorities(selectUserPermissions(orgOperationUser.getOou_id()));
        }
        return myUserDetails;
    }

完善6 web授权(即请求路径)

在WebSecurityConfig

@Override
  protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
          .antMatchers("/api/test1").hasAuthority("运维总监")
          .antMatchers("/api/test2").hasAuthority("p2")
          .antMatchers("/api/**").fullyAuthenticated()   //所有的 api开头请求必须认证通过
           .anyRequest().permitAll();

完善7:返回的token信息 添加ip地址

1.写一个类继承TokenEnhancer并重写TokenEnhancer方法

public class MyTokenEnhancer implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        final Map<String, Object> additionalInfo = new HashMap<>();
        HttpServletRequest request = ((ServletRequestAttributes) Objects
                .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        additionalInfo.put("ipAddress", request.getRemoteHost());
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
        return oAuth2AccessToken;
    }
}

2.配置类

    //自定义token返回信息
    @Bean
    TokenEnhancer tokenEnhancer(){
        return new MyTokenEnhancer();
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
        defaultAccessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());
        endpoints
                .authenticationManager(authenticationManager())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
                .userDetailsService(myUserDetailsService)
                .accessTokenConverter(defaultAccessTokenConverter)
                .tokenEnhancer(tokenEnhancer())  //自定义token返回信息
        ;
    }

完善8:客户端信息clientId,secretId从写死改成数据库读取

授权服务器的 AuthorizationServerConfig

本来写死如下:
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient("client_1")
                .secret(passwordEncoder().encode("secret_1"))
                .redirectUris("https://www.baidu.com/")
                .authorizedGrantTypes("password", "refresh_token", "authorization_code")
                .scopes("all")
                .resourceIds(USER_SERVER_RESOURCE_ID)
                .accessTokenValiditySeconds(36000)
                .refreshTokenValiditySeconds(36000)

                .and()

                .withClient("client_2")
                .secret(passwordEncoder().encode("secret_2"))
                .redirectUris("https://www.baidu.com/")
                .authorizedGrantTypes("password", "refresh_token", "authorization_code")
                .scopes("all")
                .resourceIds("resourceId2")
                .accessTokenValiditySeconds(36000)
                .refreshTokenValiditySeconds(36000)
        ;
    }

从数据读取
 1。建如下表:表名表字段都固定
 	CREATE TABLE "public"."oauth_client_details" (
	  "client_id" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "resource_ids" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "client_secret" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "SCOPE" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "authorized_grant_types" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "web_server_redirect_uri" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "authorities" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "access_token_validity" int4 DEFAULT NULL,
	  "refresh_token_validity" int4 DEFAULT NULL,
	  "additional_information" text COLLATE "pg_catalog"."default" DEFAULT NULL,
	  "autoapprove" text COLLATE "pg_catalog"."default" DEFAULT NULL
	);

2.依赖注入数据源
    @Autowired
	private DataSource dataSource;

3、客户端信息
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

4.配置
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }

完善9:token存储位置策略 内存 jwt 或者redis或者JDBC

方案1:jwt

1.添加依赖
	<!‐‐spring secuity对jwt的支持,spring cloud oauth2已经依赖,可以不配置‐‐> 
	<dependency>  		
		<groupId>org.springframework.security</groupId>  
		<artifactId>spring‐security‐jwt</artifactId>  
		<version>1.1.1.RELEASE</version>  
	</dependency>
	
2.添加JwtTokenStoreConfig文件
	@Configuration
	public class TokenStoreConfig {
	    @Autowired
	    private RedisConnectionFactory redisConnectionFactory;
	    @Autowired
	    private DataSource dataSource;
	
	    //redis存储
	    @Bean
	    public TokenStore redisTokenStore() {
	        return new RedisTokenStore(redisConnectionFactory);
	    }
	
	    //数据库存储
	    @Bean
	    public TokenStore jdbcTokenStore() {
	        return new JdbcTokenStore(dataSource);
	    }
	
	    //jwt存储
	    @Bean
	    public TokenStore jwtTokenStore() {
	        return new JwtTokenStore(jwtAccessTokenConverter());
	    }
	    @Bean
	    public JwtAccessTokenConverter jwtAccessTokenConverter() {
	        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();  //配置JWT使用的秘钥
	        accessTokenConverter.setSigningKey("123123");
	        return accessTokenConverter;
	    }
	}

3.授权服务配置文件
	@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
        defaultAccessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());
        endpoints
                .tokenStore(tokenStore)                           //指定token存储策略是jwt
                .accessTokenConverter(jwtAccessTokenConverter)    //token解析器
                .authenticationManager(authenticationManager())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
                .userDetailsService(myUserDetailsService)
                .accessTokenConverter(defaultAccessTokenConverter)
                .tokenEnhancer(tokenEnhancer())
        ;
    }

方案2:token存储在redis

1.pom加redis依赖
      <!--redis-->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-redis</artifactId>
       </dependency>

2.yml文件配置redis
   #redis
  spring:
    redis:
      host: 192.168.61.143         # Redis服务器地址
      port: 6379                   # Redis服务器连接端口
      password:
      timeout: 5000                   #连接超时时间(毫秒)
      database: 3

3.TokenStoreConfig
	@Configuration
	public class TokenStoreConfig {
	    @Autowired
	    private RedisConnectionFactory redisConnectionFactory;
	    @Autowired
	    private DataSource dataSource;
	
	    //redis存储
	    @Bean
	    public TokenStore redisTokenStore() {
	        return new RedisTokenStore(redisConnectionFactory);
	    }
	
	    //数据库存储
	    @Bean
	    public TokenStore jdbcTokenStore() {
	        return new JdbcTokenStore(dataSource);
	    }
	
	    //jwt存储
	    @Bean
	    public TokenStore jwtTokenStore() {
	        return new JwtTokenStore(jwtAccessTokenConverter());
	    }
	    @Bean
	    public JwtAccessTokenConverter jwtAccessTokenConverter() {
	        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();  //配置JWT使用的秘钥
	        accessTokenConverter.setSigningKey("123123");
	        return accessTokenConverter;
	    }
	}

4.授权服务的配置
    @Autowired
    private TokenStore redisTokenStore;
    
	@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
        defaultAccessTokenConverter.setUserTokenConverter(new MyUserAuthenticationConverter());
        endpoints
                .authenticationManager(authenticationManager())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET)
                .userDetailsService(myUserDetailsService)
                .accessTokenConverter(defaultAccessTokenConverter)
                .tokenEnhancer(tokenEnhancer())
                .tokenStore(redisTokenStore);
        ;
    }

完善10:配置说明

endpoints.tokenEnhancer(tokenEnhancer())  //自定义返回的token信息,比如添加Ip
endpoints.tokenStore(redisTokenStore)     //token存储策略

六。踩坑系列

1.密码模式控制台报错 Encoded password does not look like BCrypt

1.数据库的密码应该和授权配置的编码器一致:为BCryptPasswordEncoder编码
2.只能声明一个BCryptPasswordEncoder,其他地方要用到直接依赖注入,不要重复声明

2.密码模式能够获取到token,但是授权码模式要获取授权码提示 403

原因是在WebSecurityConfig要配置http.httpBasic() 或者 http..formLogin()

3.刚刚获取到的token,资源服务调用却显示无效

原因是授权服务指定了toekn存储在redis,资源服务没有指定就是存储在内存

授权服务指定token存储地方
 endpoints.tokenStore(redisTokenStore)

资源服务指定token存储地方
	@Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources
                .resourceId(resourceId)
                .tokenStore(redisTokenStore)
        ;
    }

注意:授权服务和资源服务要有同一个MyUserDetails

4.资源服务要和授权服务相呼应的内容

1.resourceId  
2.tokenStore     token存储位置 redis
3.MyUserDetails  相同文件(包名也一样)

貌似可以去掉的内容
  1.注册中心
  2.资源服务的配置文件 token-info-uri: http://localhost:8093/oauth/check_token   #从认证授权中心上验证token

java进阶教程2天快速入门Spring Security OAuth2.0认证授权

类似微信开放平台:

1.注册应用获取appId,appSecret
2.获取code
3.通过code获取access_token
4.通过access_token获取微信用户信息

认证的两种方式:token 和 session
1.基于session的认证,认证服务器通过后把用户信息存于session
2.基于token的认证,认证服务器通过后传token给客户端

oauth2协议中的三方
我自己
服务提供商:微信、微博、QQ
第三方软件

oauth2协议中的四个角色
资源所有者 (我)
客户端/第三方应用 (APP软件)
资源服务器 (微信、微博)
授权服务器 (颁发token)

资源添加校验规则
1.授权服务器 WebSecurityConfig类里面的configure方法
2.资源服务器的方法上加注解 @PreAuthorize() @PostAuthorize() @Secured()
3.写一个类继承ResourceServerConfigurerAdapter

测试:
1.post申请令牌:http://localhost:8589/oauth/token (body填五个 拿到token)
2.
get访问资源:http://localhost:8591/api/user/manage/search/1 Headers加,Authorization : Bearer 6825a2e9-70a0-48af-b218-c5a3b6fe5eb3
post访问资源:

在这里插入图片描述

一。总览

Spring Security所解决的是“安全访问控制”,而安全访问控制功能其实就是对所有请求进行拦截,可以通过Filter或AOP等技术来实现,Sping Security采用的就是Filter

如何进入认证授权:当初始化Spring Security时会创建一个名为SpringSecurityFilterChain的servlet过滤器,类型为FilterChainProxy(多个Filter链),
它实现了javax.servlet.Filter,因此外部的请求会经过此类作认证和授权

二。认证流程

a.认证流程简化版
//简化版认证流程:输入,输出,对比
1.用户提交(用户名,密码)SpringSecurity会将请求信息封装为Authentication  (输入)
2.根据提交的账号查询返回UsreDetails(有可能空,有可能User)   (输出)                 DaoAuthenticationProvider干活的人
3.通过PasswordEncoder对比Authentication的密码是否和UserDetails的密码一致  (对比)
b.源码阅读

Authentication 为客户输入,UsreDetails为查询

UsernamePasswordAuthenticationFilter    父类的doFilter方法打个断点,请求入口
DaoAuthenticationProvider                retrieveUser方法打个断点

获取数据库方法:DaoAuthenticationProvider类的retrieveUser方法(其实是调用UserDetailsService接口的loadUserByUsername方法)
密码校验在方法:DaoAuthenticationProvider类的additionalAuthenticationChecks方法
根据账号查询用户:UserDetailService接口里面的loadUserByUsername     UserDetailService类里面的loadUserByUsername方法(这个自定义) 表:auth_operation_user

密码编码器:在授权服务器配置文件(AuthorizationServerConfig//编码器
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

//对密码加密
String password= "123";
String hashpw = BCrypt.hashpw(password, BCrypt.gensalt());

//校验密码
BCrypt.checkpw("123",hashpw);

三。授权流程

在这里插入代码片

四。授权配置,即资源加校验规则

1.web授权设置 (在授权服务器)
//在授权服务器的WebSecurityConfig类里面的configure方法
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/oauth/**").permitAll()
            .antMatchers(("/r1/r2")).hasAnyAuthority("p1") //拥有p1权限可以访问路径/r1/r2   基于资源
            .antMatchers("/r1/r2").hasRole("xmfzr") //拥有角色xmfzr才可以访问路径/r1/r2     基于角色
            .anyRequest().authenticated()
            .and()
            .cors()
            .and()
            .csrf().disable();
}

当把.anyRequest().permitAll()放在第二行,所有请求都会直接过去
配置规律:把详细的permitAll()放在前面,把带*的放在后面

2.方法授权设置 (在资源服务器)
//在控制器上方加注解
 @PreAuthorize()  @PostAuthorize()  @Secured()
使用@Secured()注解还需要额外在类上加注解 @EnableGlobalMethodSecurity(securedEnabled = true)

五。Oauth2.0

下面是配置一个认证服务必须要实现的endpoints:
1.AuthorizationEndpoint用来作认证请求,默认url:/oauth/authorize
2.TokenEndpoint用作令牌的请求, 默认url: /oauth/token
3.资源服务对非法请求进行拦截,对请求中token进行解析鉴权等(Oauth2AuthenticationProcessingFilter实现对请求的token进行解析)

认证授权体系步骤:1、授权服务器,生成token; 2、资源服务器,校验服务

1。授权服务器配置:(做完就可以作颁发令牌和认证的测试)

配置什么:哪些客户端可以访问授权服务器,访问的路径,访问路径的安全约束
怎么实现:写一个类继承 AuthorizationServerConfigurerAdapter 并注解,里面三个方法巴拉巴拉
(下面资源服务器配置是继承 ResourceServerConfigurerAdapter)

1.声明授权服务器类(AuthorizationServerConfig)继承AuthorizationServerConfigurerAdapter,加注解@EnableAuthorizationServer@Configuration

  此类重写三个方法,功能分别是:
        客户端详情配置(哪些客户端可以访问申请令牌)
        令牌配置 (令牌访问端点和令牌怎么发放)
        令牌安全配置 (哪些url可以访问颁发令牌服务,专业讲就是安全约束)
        
2.测试:四个模式

授权码模式: 
  a.申请授权码  /uaa/oauth/authorize?client_id=c1&response_ype=code&scope=all&redirect_uri=http://www.baidu.com
     response_type:  必须,固定为code,表示这是一个授权码请求.
     client_id:     必须,在授权服务器注册应用后得到的唯一标识(如:wxappid)    
     scope:          可选,请求资源范围,多个空格隔开
     redirect_uri:   可选,授权码申请成功后跳转到此地址
     state:          可选,如果存在,返回给客户端
  b.申请令牌  
    /uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
    client_id:客户端准入标识
    client_secret:客户端密钥(与授权服务器三个配置中的客户端配置的secret(passwordEncoder().encode("nebulait"))一致)
    grant_type:授权类型,填写authorization_code表示授权码模式(授权服务器三个配置中的客户端配置要有这个           "password","refresh_token",“authorization_code”)
    code:授权码,就是钢钢获取到的授权码,注意授权码只能使用一次就无效了,需要重新申请
    redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uti一致

简化模式:response_type=token (客户端发送请求即返回一个token,应用于只有前端没有后端的场景下)
  /uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

密码模式: grant_type=password (授权服务器将令牌token发给客户端,因此应用于前端自己开发的场景下)
   /uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123
client_id:客户端准入标识(授权服务器有配置)
client_secret:客户端密钥(授权服务器有配置)
grant_type=password(授权服务器有配置)
username:资源拥有者用户名
password:资源拥有者密码


客户端模式: grant_type = client_credentials
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
client_id;客户端准入标识
client_secret:客户端密钥
grant_type:填写client_credentials表示客户端模式
//授权服务器配置文件
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

	@Override  //配置客户端
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	    clients
	            .inMemory()  //暂时使用内存模式
	            .withClient("web")  //客户端id
	            .resourceIds(OPERATION_RESOURCE_ID, TIMED_TASK_RESOURCE_ID)  //可以访问的资源列表
	            .authorizedGrantTypes("password", "refresh_token")   //允许的授权类型
	            .scopes("all")  //授权范围(客户端权限)
	            .secret(passwordEncoder().encode("nebulait"))  //客户端秘钥
	            .accessTokenValiditySeconds(36000);//Token 的有效期 10小时
	}
	
	@Override  //配置令牌 的访问端点和服务
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
	    DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();
	    defaultAccessTokenConverter.setUserTokenConverter(new CustomUserAuthenticationConverter());
	    endpoints
	            .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)  //允许Post,get提交
	            .tokenStore(tokenStore())
	            .authenticationManager(authenticationManager)   //密码模式需要
	            .userDetailsService(userDetailService)  //autorizationCodeServive 授权码模式用这个
	            .accessTokenConverter(defaultAccessTokenConverter)
	            .tokenEnhancer(new CustomTokenEnhancer())
	            .tokenServices(tokenServices())          //令牌管理服务
            .exceptionTranslator(new CustomWebResponseExceptionTranslator());
	}
	
	@Override  //配置令牌的端点约束
	public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
	    oauthServer.allowFormAuthenticationForClients()
	            .tokenKeyAccess("permitAll()")
	            .checkTokenAccess("permitAll()")
	            .checkTokenAccess("isAuthenticated()");   //允许表单认证
	}
}

/oauth/authorize: 授权,验证身份合法性
/oauth/token: 颁发令牌(申请令牌)
/oauth/confirm_access: 确认令牌提交
/oauth/error: 错误信息
/oauth/check_token: 校验令牌
/oauth/token_key:
在这里插入图片描述

2。资源服务器配置(资源加校验规则)(配置完可以作资源访问的测试)

配置什么: 通俗讲就是访问路径加权限校验
怎么实现:写一个类继承 ResourceServerConfigurerAdapter ( 上面授权服务器是AuthorizationServerConfigurerAdapter,项目中没有)

1.控制层的配置 
@PreAuthorize("hasanyAuthority('p1')") //拥有p1权限方可访问此url

2.写一个类继承ResourceServerConfigurerAdapter (项目无)

测试
 1.get请求
 heardres加 AuthorizationBearer 7930c12e-afd0-462e-9e45-b9096f09a014(token)

几种授权模式:授权码模式,密码模式,简化模式,客户端模式

六。jwt令牌

1.什么是JWT (json web token)

它是开发的行业标准,定义一种协议格式,用于通信双方传递json对象,传递信息经过数字签名可以被验证和信任。

2.为什么要有jwt令牌,能解决什么问题

当资源服务和授权服务不在一起时,资源服务使用RemoteTokenServices远程请求授权服务验证token,如果访问量较大会影响系统性能
令牌格式采用jwt即可解决上边问题:用户认证通过会得到一个Jwt令牌(有用户信息),客户端只需要携带jwt访问资源服务,资源服务根
据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

3.jwt令牌的组成

jwt令牌有三部分,分别是头部(Header),负载(PayLoad),签名(Signature),中间用(. )分隔

Header包括令牌类型和使用的hash算法
{
    "type":"JWT",
    "alg":"HS256"
}

PayLoad
{
   "sub":"123456",
   "name":"zhasgnsan",
   "admin":"true"
}

HMACSHA256{
    base64URLEncode(header) + "."       //令牌的第一部分
    base64URLEncode(payload) + "."      //令牌的第二部分
    secret      
}

4。java代码生成jwt格式token, 并且将token转成payLoad里面的map对象
1.pom引入jwt依赖
	<!--单体使用的jwt依赖 -->
	<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
    </dependency>
    
    //SpringSecurity引用的是下面的
    <!‐‐spring secuity对jwt的支持,spring cloud oauth2已经依赖,可以不配置‐‐> 
	<dependency>  		
		<groupId>org.springframework.security</groupId>  
		<artifactId>spring‐security‐jwt</artifactId>  
		<version>1.1.1.RELEASE</version>  
	</dependency>


2.生成token,代码见下面

3.官网 https://jwt.io,验证toekn
import io.jsonwebtoken.*;
import org.junit.Test;

import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author lhc
 * @Date 2021/8/13 13:39
 */
public class TestJWT {
    @Test
    public void test() {
        String jwt = generateJWT();
        Jws<Claims> claimsJws = parseJWT(jwt);
        System.out.println(claimsJws);
    }

    private String generateJWT() {
        //添加构成JWT的头部参数
        Map<String, Object> headMap = new HashMap<>();
        headMap.put("alg", SignatureAlgorithm.HS256.getValue());
        headMap.put("typ", "JWT");

        //负载
        Map<String, Object> payLoadMap = new HashMap<>();
        payLoadMap.put("userId", 1);
        payLoadMap.put("userName", "lhccs");
        payLoadMap.put("ipAddress", "localhost");

        JwtBuilder builder = Jwts.builder()
                //头部
                .setHeader(headMap)
                //负载
//                .claim("userId", 1)
//                .claim("userName", "test")
//                .claim("ipAddress", "localhost")
                .addClaims(payLoadMap)

                .setId("id")            //声明标识   "jti": "id",
                .setSubject("subject")  //声明主体   "sub": "subject",
                .setIssuedAt(new Date(System.currentTimeMillis()))  //声明签发时间
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 1000))  //声明过期时间:60s后过期

                //签名
                .signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(SecretConstant.BASE64SECRET.getBytes()));

        String jwt = builder.compact();
        System.out.println(jwt);
        return jwt;
    }


    private Jws<Claims> parseJWT(String jwt) {
        //返回的是payLoad里面的内容
        Jws<Claims> claimsJws = null;
        try {
            claimsJws = Jwts.parser().setSigningKey(Base64.getEncoder().encodeToString(SecretConstant.BASE64SECRET.getBytes())).parseClaimsJws(jwt);
            JwsHeader header = claimsJws.getHeader();
            Claims body = claimsJws.getBody();

            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date issuedAt = body.getIssuedAt();
            Date expiration = body.getExpiration();
            System.out.println("签发时间:" + sdf.format(issuedAt));
            System.out.println("过期时间:" + sdf.format(expiration));
            System.out.println("当前时间:" + sdf.format(new Date()));

        } catch (Exception e) {
            e.printStackTrace();
        }
        return claimsJws;
    }


    public interface SecretConstant {
        /**
         * 签名秘钥 自定义
         */
        String BASE64SECRET = "111111";

        /**
         * 超时毫秒数(默认30分钟)
         */

        int EXPIRESSECOND = 3600;

        /**
         * 用于JWT加密的密匙 自定义
         */

        String DATAKEY = "222222";

        /**
         * DES密钥
         */
        String DESKEY = "333333";

        /**
         * 生产管理访问本系统的约定密钥
         */
        String PRIKEY = "444444";
    }
}

5。使用jwt完成的登录校验功能
1./login  根据传过来的账号密码生成token,并将token信息存储在reponse的cookie中返回给前端(前端的业务请求在header存放这个token)
	    String jwt = JwtUtil.generateJWT(payLoadMap); //params里面账号密码登录ip等
        Cookie cookie = new Cookie("userToken", jwt);
        cookie.setPath("/");
        cookie.setMaxAge(60 * 60 * 24 * 7);
        response.addCookie(cookie);

2. /findUser  拦截器拦截业务请求,获取请求头里面的token,根据token解析成payLoadMap,获取账号密码,判断账号密码的合法性
  String jwt = request.getHeader("Authorization");
完成代码  永福的
/login
@Override
    public JSONObject login(Map<String, Object> params, HttpServletRequest request, HttpServletResponse response) {
        String userName = (String) params.get("userName");
        String userPwd = (String) params.get("userPwd");
        JSONObject data = new JSONObject();
        SysUserDto userDto = sysUserDao.getByUserName(userName);
        //该用户不存在
        if (userDto == null) {
            log.warn("【登录验证】该用户不存在!用户名:[{}]", userName);
            throw new JafException("USER_NOT_EXIST", "登录用户不存在", HttpStatus.FORBIDDEN);
        }
        //SHA1加密
        userPwd = SHA1Util.encrytSHA1(userName, userPwd);
        //密码不正确
        if (!userDto.getUserPassword().equals(userPwd)) {
            log.warn("【登录验证】用户密码校验不通过!!!!");
            throw new JafException("USER_VERIFY_FAIL", "用户密码错误", HttpStatus.METHOD_NOT_ALLOWED);
        }
        String ip = (String) params.get("ipAddress");
        log.info("【校验用户IP地址】IP地址为:[{}] ", ip);

        params.put("userId", userDto.getId());
        String jwt = JwtUtil.generateJWT(params);
        Cookie cookie = new Cookie("userToken", jwt);
        cookie.setPath("/");
        cookie.setMaxAge(60 * 60 * 24 * 7);
        response.addCookie(cookie);
        data.put("token", jwt);

        data.put("userInfo", userDto);
        checkDoubleLogin(userDto.getId(), ip);
        return data;
    }
	
	/findUser 拦截器
   @Override
    public boolean isPermit(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String jwt = request.getHeader("Authorization");
        logger.info("[登录校验拦截器]-从header中获取的jwt为:{}", jwt);
        //判断jwt是否有效
        if (StringUtils.isNotBlank(jwt)) {
            //校验jwt是否有效,有效则返回json信息,无效则返回空
            String ipAddress = JwtUtil.getClientHost(request);
            String resultJson = JwtUtil.validateLogin(jwt, ipAddress);
            logger.info("[登录校验拦截器]-校验JWT有效性返回结果:{}", resultJson);
            //resultJson为空则说明jwt超时或非法
            if (StringUtils.isNotBlank(resultJson)) {
                JSONObject jsonObject = JSONObject.parseObject(resultJson);
                String userName = (String) ((Map<String, Object>) jsonObject).get("userName");
                SysUserDto userDto = sysUserService.getByUserName(userName);
                if (userDto == null) {
                    logger.warn("[登录校验拦截器]-登录用户不存在");
                    throw new JafException("USER_NOT_EXIST", "登录用户不存在", HttpStatus.UNAUTHORIZED);
                }
                //将userDto保存在UserContext中
                UserContext.setCurrentUser(userDto);
                response.setHeader("freshToken", jsonObject.getString("freshToken"));
                checkIp(userDto.getId(), ipAddress);
                return true;
            } else {
                logger.warn("[登录校验拦截器]-JWT非法或已超时,重新登录");
            }
        }
        return false;
    }

七。加入网关

1.网关主要负责的两件事:
a.作为Oauth2.0的资源服务器角色,实现接入方权限拦截,相当于上面的 二。认证流程
b.令牌解析并转发当前用户信息(明文token)给微服务

2.微服务拿到明文token也做两件事:
a.用户授权拦截(看当前用户是否有权限访问资源)
b.将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)

八。星云授权服务器登录登出代码

1.登出

//关键语句: tokenServices.revokeToken(tokenId);  只要传入一个tokenValue,这个是org.springframework.security.oauth2框架爱提供的
@DeleteMapping(value = "/oauth/logout")
public ResponseEntity<Void> revokeToken(HttpServletRequest request) {
    String authorization = request.getHeader("Authorization");
    if (authorization != null && authorization.contains("Bearer")) {
        String tokenId = authorization.substring("Bearer".length() + 1);
        tokenServices.revokeToken(tokenId);
    }
    return new ResponseEntity<>(HttpStatus.OK);
}

2.创建token 返回如下六个信息
{
“access_token”: “ce792fb3-d0e0-4198-a03f-d44257e4ef8a”,
“token_type”: “bearer”,
“refresh_token”: “dca5c7b1-5b75-4dc0-8228-6b00cc953585”,
“expires_in”: 35058,
“scope”: “all”,
“ipAddress”: “192.168.1.72”
}

 * @param authUser            授权用户
 * @param parameters          客户端参数
 * @param authenticatedClient 授权客户端
 * @param tokenRequest        token请求
 * @return 无密码登录流程
 */
private OAuth2AccessToken dealNoPasswordLogin(
      AuthUser authUser, Map<String, String> parameters,ClientDetails authenticatedClient, okenRequest tokenRequest)
{
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
            new UsernamePasswordAuthenticationToken(serDetailService.buildCustomUserDetails(authUser, authUser.getPhone()), null);
    usernamePasswordAuthenticationToken.setDetails(parameters);
    OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authenticatedClient, tokenRequest);
    OAuth2AccessToken token = customTokenService.createAccessToken(
            new OAuth2Authentication(storedOAuth2Request, usernamePasswordAuthenticationToken));
    if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
    }
    return token;
}

3.登录相关代码

//传入三个参数: 1. HttpServletRequest request  2.客户端参数    3.授权时存储redis的key
a.将传入的request转成 authentication
b.将上一步的 authentication 连同传入的客户端参数 parameters 进行客户端相关验证 返回对象数组
c.根据key调用微信api 查询存储user信息并返回user
d.根据user,客户端参数,第二部的对象数据组成 OAuth2AccessToken

//a.利用传入的reqeust得到authentication
private Authentication attemptAuthentication(HttpServletRequest request)
        throws AuthenticationException {

    String authorization = request.getHeader("Authorization");
    String clientId = "";
    String clientSecret = "";
    if (authorization != null && authorization.contains("Basic")) {
        String clientBase64Str = authorization.substring("Basic".length() + 1);
        String clientStr = new String(Base64.getDecoder()
                .decode(clientBase64Str.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
        if (!StringUtils.isEmpty(clientStr)) {
            String[] strs = clientStr.split(":");
            clientId = strs[0];
            clientSecret = strs[1];
        }
    }

    if (clientId == null) {
        throw new BadCredentialsException("No client credentials presented");
    }

    if (clientSecret == null) {
        clientSecret = "";
    }

    clientId = clientId.trim();
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
            clientSecret);

    return authenticationManager.authenticate(authRequest);
}

//b.利用 Authentication 和客户端参数parameters 进行客户端相关验证,得到一个对象数组 object[]
/**
 * 客户端相关验证
 *
 * @param principal  登录信息
 * @param parameters 客户端参数
 * @return TokenRequest 和 ClientDetails
 */
private Object[] validClient(Principal principal, @RequestParam Map<String, String> parameters) {
    if (!(principal instanceof Authentication)) {
        throw new InsufficientAuthenticationException(
                "There is no client authentication. Try adding an appropriate authentication filter.");
    }
    String clientId = getClientId(principal);
    ClientDetails authenticatedClient = clientDetailsService.loadClientByClientId(clientId);
    TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(parameters, authenticatedClient);

    if (clientId != null && !clientId.equals("")) {
        // Only validate the client details if a client authenticated during this
        // request.
        if (!clientId.equals(tokenRequest.getClientId())) {
            // double check to make sure that the client ID in the token request is the same as that in the
            // authenticated client
            throw new InvalidClientException("Given client ID does not match authenticated client");
        }
    }
    if (authenticatedClient != null) {
        oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
    }
    return new Object[]{tokenRequest, authenticatedClient};
}

//d。利用user,客户端参数parameters , ClientDetails, TokenRequest 得到一个token
/**
 * 处理无密码登录
 *
 * @param authUser            授权用户
 * @param parameters          客户端参数
 * @param authenticatedClient 授权客户端
 * @param tokenRequest        token请求
 * @return 无密码登录流程
 */
private OAuth2AccessToken dealNoPasswordLogin(AuthUser authUser, Map<String, String> parameters,
                                              ClientDetails authenticatedClient, TokenRequest tokenRequest) {
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
            new UsernamePasswordAuthenticationToken(
                    userDetailService.buildCustomUserDetails(authUser, authUser.getPhone()), null);
    usernamePasswordAuthenticationToken.setDetails(parameters);
    OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(authenticatedClient, tokenRequest);
    OAuth2AccessToken token = customTokenService.createAccessToken(
            new OAuth2Authentication(storedOAuth2Request, usernamePasswordAuthenticationToken));
    if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
    }
    return token;
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飘然生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值