SpringBoot攻略十三、spring security oauth2服务端(password密码模式)

上一篇:【SpringBoot攻略十二、spring security集成】

1、pom.xml添加依赖

	
	
        org.springframework.security.oauth
        spring-security-oauth2
        2.3.4.RELEASE
    

	
	
	    org.springframework.boot
	    spring-boot-starter-data-redis
	

2、TestEndpoints测试用的端点即api接口

package com.javasgj.springboot.oauth.password.server;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试用的端点即api接口
 */
@RestController
public class TestEndpoints {

	@GetMapping("/product/{id}")
    public String getProduct(@PathVariable String id) {
		
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "product id : " + id;
    }

    @GetMapping("/order/{id}")
    public String getOrder(@PathVariable String id) {
    	
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return "order id : " + id;
    }
}

3、@EnableAuthorizationServer授权服务器

package com.javasgj.springboot.oauth.password.server;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 * @EnableAuthorizationServer
 * 配置授权服务器
 * 
 * 
 * 如果要是实现JWT令牌,可以参考如下(加上网上博客):
 * JWT令牌(JWT Tokens):
 * 使用JWT令牌你需要在授权服务中使用 JwtTokenStore,资源服务器也需要一个解码的Token令牌的类 JwtAccessTokenConverter,
 * JwtTokenStore依赖这个类来进行编码以及解码,因此你的授权服务以及资源服务都需要使用这个转换类。Token令牌默认是有签名的,并且资源服务需要验证这个签名,
 * 因此呢,你需要使用一个对称的Key值,用来参与签名计算,这个Key值存在于授权服务以及资源服务之中。或者你可以使用非对称加密算法来对Token进行签名,
 * Public Key公布在/oauth/token_key这个URL连接中,默认的访问安全规则是"denyAll()",即在默认的情况下它是关闭的,你可以注入一个标准的 SpEL表达式到 AuthorizationServerSecurityConfigurer这个配置中来将它开启(例如使用"permitAll()"来开启可能比较合适,因为它是一个公共密钥)。
 * 如果你要使用 JwtTokenStore,请务必把"spring-security-jwt"这个依赖加入到你的classpath中。
 */
@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

	@Autowired
	private AuthenticationManager authenticationManager;
	
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Autowired
	private DataSource dataSource;
    
    @Autowired
	private JdbcDaoImpl jdbcDaoImpl;

    /**
     * 配置令牌端点(Token Endpoint)的安全约束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    	
    	security
    		// 配置403无权限的handler,默认OAuth2AccessDeniedHandler返回一个xml错误提示
    		//.accessDeniedHandler(accessDeniedHandler)
    	
    		//配置client_secret加密方式
    		//.passwordEncoder(passwordEncoder)
    	
    		// /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话,默认是denyAll()
    		.tokenKeyAccess("permitAll()")
    		
    		// /oauth/check_token:用于资源服务访问的令牌解析验证端点,默认是denyAll()
    		.checkTokenAccess("isAuthenticated()")
    		
	    	/* 
	    	 * 认证客户端信息,校验client_id和client_secret
	    	 * allowFormAuthenticationForClients是为了注册clientCredentialsTokenEndpointFilter
	         * clientCredentialsTokenEndpointFilter,解析request中的client_id和client_secret
	         * 构造成UsernamePasswordAuthenticationToken,然后使用容器中的顶级身份管理器AuthenticationManager去进行身份认证
	         * (AuthenticationManager的实现类一般是ProviderManager。而ProviderManager内部维护了一个List,真正的身份认证是由一系列AuthenticationProvider去完成
	         * 而AuthenticationProvider的常用实现类则是DaoAuthenticationProvider,DaoAuthenticationProvider内部又聚合了一个UserDetailsService接口,UserDetailsService才是获取用户详细信息的最终接口
			 * 通过UserDetailsService实现类ClientDetailsUserDetailsService查询作简单的认证而已,这个设计是将client客户端的信息(client_id,client_secret)适配成用户的信息(username,password)
	         * 一般是针对password模式和client_credentials模式
	         * 
	         * 当然也可以使用http basic认证,如果使用了http basic认证,就不要使用clientCredentialsTokenEndpointFilter
	         * 使用BasicAuthenticationFilter过滤器,解析request header中的Authorization: Basic client_id:client_secret的Base64编码
	         * 注意:http basic认证需要在请求头添加Authorization: Basic client_id:client_secret的Base64编码
	         */    		
    		.allowFormAuthenticationForClients();
    }
    
    /**
     * 配置客户端的相关信息
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    	
    	// InMemoryClientDetailsService和JdbcClientDetailsService提供数据源给ClientDetailsUserDetailsService获取信息认证用
        // 基于内存配置客户端信息,实现类InMemoryClientDetailsService,
        /*clients.inMemory()
		        .withClient("client_1")					// clientId:用来标识客户的Id
		        //.secret("{noop}123456")				// secret:客户端安全码
		        .secret(passwordEncoder.encode("123456"))
		        .resourceIds("order")					// resourceIds:客户端对哪些资源服务可以访问,如果没设置,就是对所有的resource都有访问权限
		        .authorizedGrantTypes("password", "refresh_token")		// authorizedGrantTypes:客户端可以使用的授权类型,默认为空
		        .scopes("all")							// scope:限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围
		        .authorities("client")					// authorities:此客户端可以使用的权限(基于Spring Security authorities)
                .and()
                .withClient("client_2")
                //.secret("{noop}123456")
                .secret(passwordEncoder.encode("123456"))
                .resourceIds("order")
                .authorizedGrantTypes("client_credentials", "refresh_token")
                .scopes("all")
                .authorities("client");*/
        
        
        
        // 基于数据库配置客户端信息,实现类JdbcClientDetailsService
        clients.jdbc(dataSource);
    }

    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    	
    	/**
    	 * 配置授权端点的URL(Endpoint URLs)
    	 * defaultPath:这个端点URL的默认链接
    	 * customPath:你要进行替代的URL链接
    	 * 以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的第一个参数
    	 * 
    	 * /oauth/authorize:		授权端点,对应AuthorizationEndpoint
		 * /oauth/token:			令牌端点(获取令牌、更新令牌),对应TokenEndpoint
		 * /oauth/confirm_access:	用户确认授权提交端点,对应WhitelabelApprovalEndpoint
		 * /oauth/error:			授权服务错误信息端点,对应WhitelabelErrorEndpoint
		 * /oauth/check_token:		用于资源服务访问的令牌解析端点,对应CheckTokenEndpoint
		 * /oauth/token_key:		提供公有密匙的端点,如果你使用JWT令牌的话,,对应TokenKeyEndpoint
		 * 
		 * 这里使用默认值
    	 */
    	//endpoints.pathMapping(defaultPath, customPath)
    	
    	
    	
    	/**
    	 * TokenEndpoint源码可知,它会调用接口TokenGranter构建令牌OAuth2AccessToken
    	 * AuthorizationServerEndpointsConfigurer的tokenGranter()方法可知调用CompositeTokenGranter,获取相对应的TokenGranter(共五种)
    	 * 
    	 * ResourceOwnerPasswordTokenGranter     		password密码模式
    	 * AuthorizationCodeTokenGranter               	authorization_code授权码模式
    	 * ClientCredentialsTokenGranter                client_credentials客户端模式
    	 * ImplicitTokenGranter                         implicit简化模式 
    	 * RefreshTokenGranter                          refresh_token 刷新token
    	 * 根据grant_type判断调用哪个TokenGranter
    	 * 
    	 * AbstractTokenGranter提供grant()方法生成令牌OAuth2AccessToken
    	 * 上面五种TokenGranter都是继承AbstractTokenGranter并且覆盖相应的方法已达到各自的要求,如:
    	 * password密码模式:ResourceOwnerPasswordTokenGranter
    	 * 		覆盖getOAuth2Authentication方法,认证用户名和密码,返回OAuth2Authentication(实现Authentication存储用户信息)
    	 * 
    	 * 
    	 * 
    	 */
        endpoints
        		// password模式需要authenticationManager
        		.authenticationManager(authenticationManager)
        		// 必须指定,否则refresh_token会报错
        		.userDetailsService(jdbcDaoImpl)
        		
        		/**
        		 *  TokenServices真正生成OAuth2AccessToken,默认DefaultTokenServices,
        		 *  里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,
        		 *  除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore这个接口有一个默认的实现,
        		 *  它就是 InMemoryTokenStore,所有的令牌是被保存在了内存中
        		 */
        		//.tokenServices(tokenServices)
        		
        		/**
        		 * 存储token
        		 * InMemoryTokenStore   		基于内存,默认值
        		 * JdbcTokenStore            	基于数据库
        		 * JwtTokenStore              	基于Jwt
				 * RedisTokenStore           	基于redis,可以利用redis自身key的过期时间
        		 */
        		.tokenStore(new RedisTokenStore(redisConnectionFactory));
    }
}

4、@EnableResourceServer资源服务器

package com.javasgj.springboot.oauth.password.server;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.ResourceServerSecurityConfigurer;

/**
 * @EnableResourceServer
 * 配置资源服务器
 *
 * 主要过滤器:OAuth2AuthenticationProcessingFilter
 * 使用AuthenticationManager的实现类OAuth2AuthenticationManager认证token,使用Authentication的实现类OAuth2Authentication来填充Spring Security上下文。
 * 
 * TokenExtractor:只有一个实现类BearerTokenExtractor
 * 它的作用在于分离出请求中包含的token。也启示了我们可以使用多种方式携带token
 * request Header中:Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
 * url拼接:http://localhost:8080/order/1?access_token=f732723d-af7f-41bb-bd06-2636ab2be135
 * form表单(post):access_token=f732723d-af7f-41bb-bd06-2636ab2be135
 */
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

	@Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
		
        resources
        	// 指定资源服务的resourceid(推荐),配合clients的resourceIds
        	.resourceId("order")
        	
        	// 只能基于token认证
        	.stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        
        http
        	.sessionManagement()
    		.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
    		.and()
            .authorizeRequests()
            .antMatchers("/order/**").hasRole("USER")						// 用户角色,.authenticated()只需认证无需授权都可以访问
            .antMatchers("/product/**").access("#oauth2.hasScope('all')")	// 指定资源访问范围,配合clients的scopes
            //.antMatchers("/product/**").hasRole("USER")
            //.antMatchers("/product/**").hasAnyRole("USER", "ADMIN")
            //.antMatchers("/product/**").access("hasRole('USER') or hasRole('ADMIN')")
            .anyRequest().permitAll()
            .and()
            .csrf().disable();
    }
}

5、@EnableWebSecurity spring security配置

package com.javasgj.springboot.oauth.password.server;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private DataSource dataSource;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		
		// 测试,用户保存在内存中
		/*auth.inMemoryAuthentication()
            	.withUser("user_1").password("{noop}123456").roles("USER")
                .and()
                .withUser("user_2").password("{noop}123456").roles("USER");*/
		
		// 数据库加载用户
		auth
			//.eraseCredentials(false)						// 是否清空凭证即密码
			.userDetailsService(jdbcDaoImpl())				// 指定获取用户信息的userDetailsService
			.passwordEncoder(passwordEncoder());			// 设置加密方式
	}

	/**
	 * 暴露AuthenticationManager,给OAuth2AuthorizationServerConfig的password模式使用
	 */
	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		
		AuthenticationManager manager = super.authenticationManagerBean();
		return manager;
	}
	
	@Bean
	public JdbcDaoImpl jdbcDaoImpl() {
		
		JdbcDaoImpl jdbcDaoImpl = new JdbcDaoImpl();
		jdbcDaoImpl.setDataSource(dataSource);
		jdbcDaoImpl.setRolePrefix("ROLE_");
		jdbcDaoImpl.setEnableGroups(true);
		jdbcDaoImpl.setUsersByUsernameQuery("select username, password, enabled from t_system_user where username = ?");
		jdbcDaoImpl.setAuthoritiesByUsernameQuery("select username, privilege_code from t_system_user_privilege where username = ?");
		jdbcDaoImpl.setGroupAuthoritiesByUsernameQuery("select a.id, a.name, c.privilege_code from t_system_role a, t_system_user_role b, t_system_role_privilege c where a.id = b.role_id and b.role_id = c.role_id and b.username = ?");
		
		return jdbcDaoImpl;
	}
	
	/**
	 * 一旦暴露PasswordEncoder,全局默认的无加密方式就会失效,所以授权服务clients的密码也需要用此方式加密
	 * @return
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		
		return new BCryptPasswordEncoder();
	}
}

6、启动类Application

package com.javasgj.springboot.oauth.password.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * spring boot应用启动类
 */
@SpringBootApplication
public class Application {

	public static void main(String[] args) {
		
		SpringApplication.run(Application.class, args);
	}
}

7、测试postman
访问:http://127.0.0.1:8080/springboot/order/1
提示:
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第1张图片
接着,我们使用postman
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第2张图片
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第3张图片
这样我们就获取到token,那么访问资源时带上token应该就能成功了:
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第4张图片
更新令牌
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第5张图片
注意:
scope说明

当客户配置scope=all,select
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第6张图片
获取token时没有指定scope,那么这个token拥有所有权限即all和select,那么下面资源可以访问
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第7张图片
因为product/**需要all的权限;

如果获取token时指定scope=select,那么这个token只拥有select权限而没有all的权限,product/**需要all的权限就不能访问了,返回如下错误:
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第8张图片
令牌更新会继承第一次获取token时的scope!

很有用的两张图

SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第9张图片
SpringBoot攻略十三、spring security oauth2服务端(password密码模式)_第10张图片

你可能感兴趣的:(SpringBoot)