什么是认证:
什么是会话:
基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。
什么是授权:
认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。
授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有 权限则拒绝访问。
授权的数据模型:
RBAC:
业界通常基于RBAC实现授权。
根据下边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。
if(主体.hasRole("总经理角色id")){
查询工资
}
如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是总经理或部门经理”,修改代码如下:
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。
if(主体.hasPermission("查询工资权限标识")){
查询工资
}
总结来说就是基于资源比基于角色拥有更小的粒度,因此更好扩展。
认证流程:
方法 | 含义 |
---|---|
HttpSession | getSession(Boolean create) 获取当前HttpSession对象 |
void | setAttribute(String name,Object value) 向session中存放对象 |
object | getAttribute(String name) 从session中获取对象 |
void | removeAttribute(String name); 移除session中对象 |
void | invalidate() 使HttpSession失效 |
略 | … |
Spring Security介绍:
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring‐security‐webartifactId>
<version>5.1.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring‐security‐configartifactId>
<version>5.1.4.RELEASEversion>
dependency>
认证页面:
安全配置:
(1)url匹配/r/**的资源,经过认证后才能访问。
(2)其他url完全开放。
(3)支持form表单认证,认证成功后转向/login-success。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
//配置安全拦截机制 @Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
(1) .antMatchers("/r/**").authenticated()
(2) .anyRequest().permitAll()
.and()
(3) .formLogin().successForwardUrl("/login‐success");
}
}
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] {
ApplicationConfig.class, WebSecurityConfig.class};
}
Spring Security初始化:
public class SpringSecurityApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
public SpringSecurityApplicationInitializer() {
//super(WebSecurityConfig.class);
}
}
默认根路径请求:
//默认Url根路径跳转到/login,此url为spring security提供
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/login");
}
认证成功页面:
//配置安全拦截机制 @Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
(1) .antMatchers("/r/**").authenticated()
(2) .anyRequest().permitAll()
.and()
(3) .formLogin().successForwardUrl("/login‐success");
}
@RequestMapping(value = "/login‐success",
produces = {
"text/plain;charset=UTF‐8"})
public String loginSuccess(){
return " 登录成功";
}
Demo测试:
授权:
/**
* 测试资源1 * @return */
@GetMapping(value = "/r/r1",produces = {
"text/plain;charset=UTF‐8"}) public String r1(){
return " 访问资源1";
}
/**
* 测试资源2 * @return */
@GetMapping(value = "/r/r2",produces = {
"text/plain;charset=UTF‐8"}) public String r2(){
return " 访问资源2";
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin().successForwardUrl("/login‐success"); }
集成SpringBoot:
安全配置:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//内容跟Spring security入门程序一致
}
结构总览:
Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,SpringSecurity对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain的Servlet过滤器,类型为
org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)
和决策管理器 (AccessDecisionManager)
进行处理,下图是FilterChainProxy相关类的UML图示。
下面介绍过滤器链中主要的几个过滤器及其作用:
①SecurityContextPersistenceFilter
这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
②UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
③FilterSecurityInterceptor
是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前 面已经详细介绍过了;
④ExceptionTranslationFilter
能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
AuthenticationProvider:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> var1);
}
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
(1)
Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。(2)
getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系 列字符串。(3)
getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。(4)
getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地 址和sessionId的值。(5)
getPrincipal(),身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细 信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。public interface Authentication extends Principal,
Serializable {
(1)
Collection<? extends GrantedAuthority>
getAuthorities(); (2)
Object getCredentials(); (3)
Object getDetails(); (4)
Object getPrincipal(); (5)
boolean isAuthenticated();
void setAuthenticated(boolean var1)
throws IllegalArgumentException; }
UserDetailsService:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled(); }
它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形 成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了 AuthenticationProvider认证之后被填充的。通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
SpringSecurity提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。
PasswordEncoder:
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword){
return false;
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
授权流程:
http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
...
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication authentication , Object object,
Collection<ConfigAttribute> configAttributes ) throws
AccessDeniedException, InsufficientAuthenticationException;
//略..
}
授权决策:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = ‐1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3); }
自定义认证:
@Override
protected void configure(HttpSecurity http) throws Exception {
//屏蔽CSRF控制,即spring security不再限制CSRF
http.csrf().disable()
...
}
<form action="login" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
...
</form>
会话:
spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
会话控制:
机制 | 描述 |
---|---|
always | 如果没有session存在就创建一个 |
ifRequired | 如果需要就创建一个Session(默认)登录时 |
never | SpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么Spring Security将会使用它。 |
stateless | SpringSecurity将绝对不会创建Session,也不使用Session |
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) }
会话超时:
server.servlet.session.timeout=3600s
http.sessionManagement()
.expiredUrl("/login‐view?error=EXPIRED_SESSION")
.invalidSessionUrl("/login‐view?error=INVALID_SESSION");
安全会话cookie:
server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true
退出:
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login‐view?logout");
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//...
.and()
(1) .logout()
(2) .logoutUrl("/logout")
(3) .logoutSuccessUrl("/login‐view?logout")
(4) .logoutSuccessHandler(logoutSuccessHandler)
(5) .addLogoutHandler(logoutHandler)
(6) .invalidateHttpSession(true);
}
授权:
方法授权:
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...}
public interface BankService {
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();
@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}
public interface BankService {
@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);
@PreAuthorize("isAnonymous()")
public Account[] findAccounts();
@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
public Account post(Account account, double amount);
}
什么是分布式系统:
分布式认证需求:
统一认证授权:
应用接入认证:
分布式认证方案:
总体来讲,基于session认证的认证方式,可以更好的在服务端对会话进行控制,且安全性较高。但是,session机 制方式基于cookie,在复杂多样的移动客户端上不能有效的使用,并且无法跨域,另外随着系统的扩展需提高 session的复制、黏贴及存储的容错性。
技术方案:
统一认证服务(UAA):
它承载了OAuth2.0接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。API网关:
作为系统的唯一入口,API网关为接入方提供定制的API集合,它可能还具有其它职责,如身份验证、监控、负载均 衡、缓存等。API网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所 有的非业务功能。OAuth2.0介绍:
<1>客户端请求第三方授权:
<2>资源拥有者同意给客户端授权:
<3>客户端获取到授权码,请求认证服务器申请令牌:
<4>认证服务器向客户端响应令牌:
<5>客户端请求资源服务器的资源:
<6>资源服务器返回受保护资源:
客户端:
本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏 览器端)、微信客户端等。资源拥有者:
通常为用户,也可以是应用程序,即该资源的拥有者。授权服务器(也称认证服务器):
用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。资源服务器:
存储资源的服务器,本例子为微信存储的用户信息。<1> client_id:客户端标识
<2> client_secret:客户端秘钥
环境介绍:
下面是配置一个认证服务必须要实现的endpoints:
AuthorizationEndpoint
服务于认证请求。默认 URL:/oauth/authorize。TokenEndpoint
服务于访问令牌的请求。默认 URL:/oauth/token。环境搭建:
-----本工程采用SpringBoot开发,每个工程编写一个启动类:------
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
@EnableFeignClients(basePackages =
{
"com.itheima.security.distributed.uaa"})
public class UAAServer {
public static void main(String[] args) {
SpringApplication.run(UAAServer.class, args);
}
}
---------配置文件在resources下创建application.properties----
spring.application.name=uaa‐service
server.port=53020
spring.main.allow‐bean‐definition‐overriding = true
logging.level.root = debug
logging.level.org.springframework.web = info
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF‐8
spring.http.encoding.force = true
server.tomcat.remote_ip_header = x‐forwarded‐for
server.tomcat.protocol_header = x‐forwarded‐proto
server.use‐forward‐headers = true
server.servlet.context‐path = /uaa
spring.freemarker.enabled = true
spring.freemarker.suffix = .html
spring.freemarker.request‐context‐attribute = rc
spring.freemarker.content‐type = text/html
spring.freemarker.charset = UTF‐8
spring.mvc.throw‐exception‐if‐no‐handler‐found = true
spring.resources.add‐mappings = false
spring.datasource.url = jdbc:mysql://localhost:3306/user_db?useUnicode=true
spring.datasource.username = root
spring.datasource.password = mysql
spring.datasource.driver‐class‐name = com.mysql.jdbc.Driver
#eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/ #eureka.instance.preferIpAddress = true
#eureka.instance.instance‐id = ${
spring.application.name}:${
spring.cloud.client.ip‐ address}:${
spring.application.instance_id:${
server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime‐types[0] = text/xml
feign.compression.request.mime‐types[1] = application/xml
feign.compression.request.mime‐types[2] = application/json
feign.compression.request.min‐request‐size = 2048
feign.compression.response.enabled = true
---------在resources中创建application.properties----------
spring.application.name=order‐service
server.port=53021
spring.main.allow‐bean‐definition‐overriding = true logging.level.root = debug
logging.level.org.springframework.web = info
spring.http.encoding.enabled = true
spring.http.encoding.charset = UTF‐8
spring.http.encoding.force = true
server.tomcat.remote_ip_header = x‐forwarded‐for
server.tomcat.protocol_header = x‐forwarded‐proto
server.use‐forward‐headers = true
server.servlet.context‐path = /order
spring.freemarker.enabled = true
spring.freemarker.suffix = .html
spring.freemarker.request‐context‐attribute = rc
spring.freemarker.content‐type = text/html
spring.freemarker.charset = UTF‐8
spring.mvc.throw‐exception‐if‐no‐handler‐found = true
spring.resources.add‐mappings = false
#eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/ #eureka.instance.preferIpAddress = true
#eureka.instance.instance‐id = ${
spring.application.name}:${
spring.cloud.client.ip‐ address}:${
spring.application.instance_id:${
server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime‐types[0] = text/xml
feign.compression.request.mime‐types[1] = application/xml
feign.compression.request.mime‐types[2] = application/json
feign.compression.request.min‐request‐size = 2048
feign.compression.response.enabled = true
授权服务器配置:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends
AuthorizationServerConfigurerAdapter {
//略...
}
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
public AuthorizationServerConfigurerAdapter() {
}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
配置客户端详细信息:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// clients.withClientDetails(clientDetailsService);
clients.inMemory()// 使用in‐memory存储
.withClient("c1")// client_id
.secret(new BCryptPasswordEncoder().encode("secret"))
.resourceIds("res1")
.authorizedGrantTypes("authorization_code",
"password","client_credentials","implicit","refresh_token")
// 该client允许的授权类型 authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all")// 允许的授权范围
.autoApprove(false)
//加上验证回调地址
.redirectUris("http://www.baidu.com"); }
管理令牌:
InMemoryTokenStore:
这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。JdbcTokenStore:
这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。JwtTokenStore:
这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对 于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授 权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);
service.setSupportRefreshToken(true);
service.setTokenStore(tokenStore);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
令牌访问端点配置:
配置授权类型(Grant Types):
AuthorizationServerEndpointsConfigurer 通过设定以下属性决定支持的授权类型(Grant Types):authenticationManager:
认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置 这个属性注入一个 AuthenticationManager 对象。userDetailsService:
如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对 象),当你设置了这个之后,那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查,用 来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。authorizationCodeServices:
这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对 象),主要用于 “authorization_code” 授权码类型模式。implicitGrantService:
这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。tokenGranter:
当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并 且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的 需求的时候,才会考虑使用这个。 @Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
//设置授权码模式的授权码如何 存取,暂时采用内存方式
return new InMemoryAuthorizationCodeServices();
}
令牌端点的安全约束:
@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
.tokenKeyAccess("permitAll()") (1)
.checkTokenAccess("permitAll()") (2)
.allowFormAuthenticationForClients() (3)
;
}
授权服务配置总结:
web安全配置:
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
;
}
}
授权码模式:
(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:
①参数列表如下:
<1>client_id:客户端准入标识。
<2>response_type:授权码模式固定为code。 scope:客户端权限。
<3>redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
/uaa/oauth/token?
client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://w ww.baidu.com
授权码模式测试:
http://localhost:53020/uaa/oauth/authorize?
client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
POST http://localhost:53020/uaa/oauth/token
/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
授权码简化模式测试:
http://localhost:53020/uaa/oauth/authorize?
client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com
http://aa.bb.cc/receive#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbn...
密码模式:
/uaa/oauth/token?
client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123
密码模式测试:
POST http://localhost:53020/uaa/oauth/token
客户端模式:
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
客户端模式测试:
POST http://localhost:53020/uaa/oauth/token
资源服务案例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import
org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import
org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerA dapter;
import
org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfi gurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenServices(tokenService())
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")// /oauth/token_key 安全配置
.checkTokenAccess("permitAll()") // /oauth/check_token 安全配置
}
//资源服务令牌解析服务 @Bean
public ResourceServerTokenServices tokenService() {
//使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret RemoteTokenServices service=new RemoteTokenServices();
service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
service.setClientId("c1");
service.setClientSecret("secret"); return service;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenServices(tokenService())
.stateless(true);
}
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAnyAuthority('p1')")
public String r1(){
return "访问资源1";
}
}
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//安全拦截机制(最重要) @Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
// .antMatchers("/r/r1").hasAuthority("p2")
// .antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
;
}
}
资源服务测试:
响应结果: {
"access_token": "e3360db3‐2d85‐41c9‐ac5e‐b9adba48e26c",
"token_type": "bearer",
"expires_in": 7199,
"scope": "all"
}
{
"error": "invalid_token",
"error_description": "f3360db3‐2d85‐41c9‐ac5e‐b9adba48e26c"
}
JWT介绍:
JWT令牌结构:
{
"alg": "HS256",
"typ": "JWT"
}
{
"sub": "1234567890",
"name": "456",
"admin": true
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
配置JWT令牌服务:
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
return converter;
}
}
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);
service.setSupportRefreshToken(true);
service.setTokenStore(tokenStore);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
校验jwt令牌:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Autowired
TokenStore tokenStore;
//资源服务令牌解析服务
// @Bean
// public ResourceServerTokenServices tokenService() {
// //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
// RemoteTokenServices service=new RemoteTokenServices();
// service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token"); // service.setClientId("c1");
// service.setClientSecret("secret"); // return service;
// }
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
.stateless(true);
}
完善环境配置:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
/**
* 1.客户端详情相关配置
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder());
return clientDetailsService; }
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
/**
* 2.配置令牌服务(token services)
*/
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore); //绑定tokenStore
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
/**
* 3.配置令牌(token)的访问端点
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);//设置授权码模式的授权码如何存取
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenService())
.allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
/**
* 4.配置令牌端点(Token Endpoint)的安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security){
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients()//允许表单认证
;
}
}
回顾技术方案如下:
注册中心:
spring:
application:
name: distributed‐discovery
server:
port: 53000 #启动端口
eureka:
server:
enable‐self‐preservation: false #关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保 护,导致别人还认为是好用的服务
eviction‐interval‐timer‐in‐ms: 10000 #清理间隔(单位毫秒,默认是60*1000)5秒将客户端剔除的服务在服 务注册列表中剔除#
shouldUseReadOnlyResponseCache: true #eureka是CAP理论种基于AP策略,为了保证强一致性关闭此切换CP 默认不关闭 false关闭
client:
register‐with‐eureka: false #false:不作为一个客户端注册到注册中心
fetch‐registry: false #为true时,可以启动,但报异常:Cannot execute request on any known server
instance‐info‐replication‐interval‐seconds: 10
serviceUrl:
defaultZone: http://localhost:${server.port}/eureka/
instance:
hostname: ${spring.cloud.client.ip‐address}
prefer‐ip‐address: true
instance‐id: ${spring.application.name}:${spring.cloud.client.ip‐ address}:${spring.application.instance_id:${server.port}}
package com.itheima.security.distributed.discovery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication @EnableEurekaServer
public class DiscoveryServer {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServer.class, args);
}
}
网关:
spring.application.name=gateway‐server server.port=53010
spring.main.allow‐bean‐definition‐overriding = true logging.level.root = info
logging.level.org.springframework = info
zuul.retryable = true
zuul.ignoredServices = *
zuul.add‐host‐header = true
zuul.sensitiveHeaders = *
zuul.routes.uaa‐service.stripPrefix = false
zuul.routes.uaa‐service.path = /uaa/**
zuul.routes.order‐service.stripPrefix = false
zuul.routes.order‐service.path = /order/**
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance‐id = ${spring.application.name}:${spring.cloud.client.ip‐ address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime‐types[0] = text/xml
feign.compression.request.mime‐types[1] = application/xml
feign.compression.request.mime‐types[2] = application/json
feign.compression.request.min‐request‐size = 2048
feign.compression.response.enabled = true
zuul.routes.uaa‐service.stripPrefix = false
zuul.routes.uaa‐service.path = /uaa/**
zuul.routes.user‐service.stripPrefix = false
zuul.routes.user‐service.path = /order/**
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServer {
public static void main(String[] args) {
SpringApplication.run(GatewayServer.class, args);
}
}
token配置:
package com.itheima.security.distributed.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "uaa123";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来解密
return converter;
}
}
配置资源服务:
@Configuration
public class ResouceServerConfig {
public static final String RESOURCE_ID = "res1";
/**
* 统一认证服务(UAA) 资源拦截
*/
@Configuration
@EnableResourceServer
public class UAAServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uaa/**").permitAll();
}
}
/**
* 订单服务
*/
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
}
}
}
安全配置:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and().csrf().disable();
}
}
转发明文token给微服务:
/**
* token传递拦截
*/
public class AuthFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public Object run() {
/**
* 1.获取令牌内容
*/
RequestContext ctx = RequestContext.getCurrentContext();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(!(authentication instanceof OAuth2Authentication)){
// 无token访问网关内资源的情况,目 前仅有uua服务直接暴露
return null;
}
OAuth2Authentication oauth2Authentication = (OAuth2Authentication)authentication;
Authentication userAuthentication = oauth2Authentication.getUserAuthentication();
Object principal = userAuthentication.getPrincipal();
/**
* 2.组装明文token,转发给微服务,放入header,名称为json‐token
*/
List<String> authorities = new ArrayList();
userAuthentication.getAuthorities().stream().forEach(s ‐ >authorities.add(((GrantedAuthority) s).getAuthority()));
OAuth2Request oAuth2Request = oauth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
if(userAuthentication != null){
jsonToken.put("principal",userAuthentication.getName());
jsonToken.put("authorities",authorities);
}
ctx.addZuulRequestHeader("json‐token",
EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}
package com.itheima.security.distributed.gateway.config;
import com.itheima.security.distributed.gateway.filter.AuthFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class ZuulConfig {
@Bean
public AuthFilter preFileter() {
return new AuthFilter();
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(18000L);
source.registerCorsConfiguration("/**", config);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
微服务用户鉴权拦截:
@PreAuthorize("hasAuthority('p1')")
@GetMapping(value = "/r1")
public String r1(){
UserDTO user = (UserDTO)
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "访问资源1";
}
@PreAuthorize("hasAuthority('p2')")
@GetMapping(value = "/r2")
public String r2(){
//通过Spring Security API获取当前登录用户
UserDTO user =
(UserDTO)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "访问资源2";
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("json‐token"); if (token != null){
//1.解析token
String json = EncryptUtil.decodeUTF8StringBase64(token);
JSONObject userJson = JSON.parseObject(json);
UserDTO user = new UserDTO();
user.setUsername(userJson.getString("principal"));
JSONArray authoritiesArray = userJson.getJSONArray("authorities");
String [] authorities = authoritiesArray.toArray( new
String[authoritiesArray.size()]);
//2.新建并填充authentication
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
user, null, AuthorityUtils.createAuthorityList(authorities));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( httpServletRequest));
//3.将authentication保存进安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
重点回顾: