Spring Security初体验

Spring Security

您需要 认证(你是谁) 授权(你能干什么) 攻击防护 (防止伪造身份) 管理会话等等,会涉及到的知识点

  • Spring Session(分布式Session [集群,单机])
  • OAuth协议
  • Spring Social
  • JWT (Json Web Token)
  • SSO
  • Rbac …… and so on ...

与shrio的认识与区别

  • shrio因为是Apache下的一个项目,不跟任何容器和框架绑定,可以独立运行,所以相对来说简便灵活

  • spring security较好的解决了功能级权限管理的问题,并且spring security对Oauth、OpenID也有支持,Shiro则需要自己手动实现

Spring Security 基本原理

先来用用吧,慢慢的原理就自然懂了 ……

  • 我们先来看看,不自定义任何spring security的配置 ,它给我们带来的默认的处理效果
## 启用 security basic
security.basic.enabled = true
-- 在服务器启动日志中可以看到
Using default security password : uuid 

在访问任何服务接口时都会弹出一个【需要进行身份验证】的窗口,要求输入默认用户 user的密码(就是日志中生成的那个) ,这就表明 spring security 默认拦截了所有的接口进行了 http basic 认证

  • 表单登录 formlogin

    @Configuration
    public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Override
    	protected void configure(HttpSecurity http) throws Exception {
            
    		// http.httpBasic()
             http.formLogin()
                 .and()
                 .authorizeRequests()
                 .anyRequest()
                 .authenticated();
        }     
         …………       
    }
    
    graph TD; 请求-->|filter|FilterSecurityInterceptor; FilterSecurityInterceptor --> |exception|Login页面; Login页面-->|输入登录信息|UsernamePasswordAuthenticationFilter; UsernamePasswordAuthenticationFilter-->|错误登录|Login页面; UsernamePasswordAuthenticationFilter-->|正确登录信息|FilterSecurityInterceptor; FilterSecurityInterceptor-->|yes|Controller-restapi;
  • 原理

    其实原理就是 spring security 在用户访问 rest api 之前加了 spring security 过滤器链

    sequenceDiagram participant 客户端 participant spring security 客户端->>spring security: 客户端请求 客户端-->>RESTAPI: 我是请求,我被 spring security过滤了 Note over 客户端: 如果没启用spring security<br/>我可以直接访问RESTAPI Note over spring security: 我是spring security 过滤器链 <br/> 1.UsernamePasswordAuthenticationFilter<br/>2.BasicAuthenticationFilter<br/>3.xxxFilter ……<br/>4.ExceptionTranslationFilter<br/>5.FilterSecurityInterceptor(最后的守门员Filter) Note over RESTAPI: 受保护的apis... spring security->>RESTAPI: 过滤后重定向到客户的原生请求 RESTAPI->>spring security: RESTAPI接口服务处理后数据继续经过spring security spring security->>客户端: 我带来了你想请求的数据 RESTAPI-->> 客户端: 接口返回!

OAuth协议

  • 怎么产生的

    • 假如第3方应用想要你存储在某个社交应用上的某些数据,你不可能把用户名和密码给它
    • 如果给它,你没办法限制它只访问 某些 数据
    • 如果你不再同意它访问你的数据,只能修改密码
    • 密码很容易泄漏
    • 这时候OAuth协议出来了
      • 它定义一个TOKEN
      • TOKEN包含有效期,可访问的信息,用户身份信息
  • 角色

    • 服务提供商([认证服务器,资源服务器])

      • 服务提供商 Provider 比如微信
      • 认证服务器 (Authorization Server) 和资源服务器 (Resource Server) 可以在同一机器上
      • 资源服务器就是存 资源所有者 数据的地方
    • 资源所有者

    • 第3方应用Client

  • 执行流程

    graph TD; Resource-Owner -->|0.访问|第三方应用Client; 第三方应用Client -->|1.请求授权|Resource-Owner; Resource-Owner -->|2.同意授权|第三方应用Client; 第三方应用Client -->|3.Get TOKEN|Authorization-Server; Authorization-Server -->|4.确认用户同意后,发放TOKEN|第三方应用Client; 第三方应用Client --> |5.使用TOKEN获取资源|Resource-Server; Resource-Server --> |6.验证TOKEN开放资源|第三方应用Client;
  • 授权方式

    上图 执行流程中第2步 同意授权的方式有如下几种

    • 授权码 (基本都是使用这种方式)

      如图(授权码方式流程图):

      graph LR; Resource-Owner -->|0.访问|第三方应用Client; 第三方应用Client -->|1.将用户导向认服务器|Authorization-Server; Resource-Owner -->|2.用户同意授权|Authorization-Server; Authorization-Server -->|3.返回授权码|第三方应用Client; 第三方应用Client -->|4.申请令牌|Authorization-Server; Authorization-Server -->|5.发放TOKEN|第三方应用Client; 第三方应用Client --> |6.使用TOKEN获取资源|Resource-Server; Resource-Server --> |7.验证TOKEN开放资源|第三方应用Client;
      • 密码

      • 简化

      • 客户端模式

Spring Social

逻辑流程图

通俗地讲就是第3方登录 ,我们想一下如果把 上面(授权码方式流程图)步骤6 中 资源 换成用户信息如下:

  • Resource-Server : 包含在 服务提供商(Service Provider)
graph LR; 第三方应用Client --> |6.使用TOKEN获取用户信息|Resource-Server; Resource-Server --> |7.用户信息|Spring-Social; Spring-Social --> |8.根据用户信息构建Authentication<br/>并存入到SecurityContext中<br/>以供第3方登录<br/>这一步全是在第3方应用中完成的|第三方应用Client;

这样就完成了 经过用户授权 用社交应用(微信)上的用户信息到 第3方应用(爱奇艺)上登录的过程

Spring Social 已经帮我们封装了大量的步骤 我们大致只需要实现以下几个步骤就可以拿到用户信息

  1. 定义一个用户实体

  2. 实现AbstractAuth2ApiBinding

    • 需要传递 oauth2.0的通用参数

      • ACCESS_TOKEN (调用入参Spring Social已经把我们获取了)

      • appid (调用入参)

      • OPENID(通过ACCESS_TOKEN 获取)

    • 获取社交服务上的用户信息接口

  3. 实现AbstractOAuth2ServiceProvider<T>,泛型T就是步骤2中的实现类接口

    • 构造需要传递参数

      • appid (申请应用时配置的)

      • appsecret(申请应用时配置的)

      • authorizedUrl (引导用户授权认证的地址,每个服务提供商地址不一样,但是基本不变)

      • accessTokenUrl( 申请令牌的地址)

  4. 实现ApiAdapter<T>,泛型T就是步骤2中的实现类接口

    • 目的就是适配 个性化服务提供商用户数据结构 到 spring social的用户数据
    • 重写方法,注入或转换数据
      • 接口T中获取的数据转换到 ConnectionValues中
  5. 实现OAuth2ConnectionFactory<T>,泛型T就是步骤2中的实现类接口

  • 构造需要传递参数
    • appid (申请应用时配置的)

    • appsecret(申请应用时配置的)

    • providerId(配置提供 服务提供商唯一标识)

    • ServiceProvider对象 ( 传入步骤3中定义的实现类对象 )

  1. 把转换的数据 通过构建 connection 保存到数据库 SocialConfig

    
    

@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {

@Autowired
private DataSource dataSource;
@Autowired
private SecurityProperties securityProperties;
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;

@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
	JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
			connectionFactoryLocator, Encryptors.noOpText());
	repository.setTablePrefix("wesley_");
	if(connectionSignUp != null) {
		repository.setConnectionSignUp(connectionSignUp);
	}
	return repository;
}

@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
	String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl();
	ImoocSpringSocialConfigurer configurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
	configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl());
	return configurer;
}

@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator) {
	return new ProviderSignInUtils(connectionFactoryLocator,
			getUsersConnectionRepository(connectionFactoryLocator)) {
	};
}

}
```

QQ登录

public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
	
	private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
	
	private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
	
	private String appId;
	private String openId;
	private ObjectMapper objectMapper = new ObjectMapper();
	
	public QQImpl(String accessToken, String appId) {
		super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
		this.appId = appId;
		String url = String.format(URL_GET_OPENID, accessToken);
		String result = getRestTemplate().getForObject(url, String.class);
		System.out.println(result);
		this.openId = StringUtils.substringBetween(result, "\"openid\":\"", "\"}");
	}
	
	@Override
	public QQUserInfo getUserInfo() {
		String url = String.format(URL_GET_USERINFO, appId, openId);
		String result = getRestTemplate().getForObject(url, String.class);
		System.out.println(result);
		QQUserInfo userInfo = null;
		try {
			userInfo = objectMapper.readValue(result, QQUserInfo.class);
			userInfo.setOpenId(openId);
			return userInfo;
		} catch (Exception e) {
			throw new RuntimeException("获取用户信息失败", e);
		}
	}

}
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {

	private String appId;
	
	private static final String URL_AUTHORIZE = "https://graph.qq.com/oauth2.0/authorize";
	
	private static final String URL_ACCESS_TOKEN = "https://graph.qq.com/oauth2.0/token";
	
	public QQServiceProvider(String appId, String appSecret) {
		super(new QQOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
		this.appId = appId;
	}
	
	@Override
	public QQ getApi(String accessToken) {
		return new QQImpl(accessToken, appId);
	}

}

public class QQAdapter implements ApiAdapter<QQ> {

	@Override
	public boolean test(QQ api) {
		return true;
	}

	@Override
	public void setConnectionValues(QQ api, ConnectionValues values) {
		QQUserInfo userInfo = api.getUserInfo();
		
		values.setDisplayName(userInfo.getNickname());
		values.setImageUrl(userInfo.getFigureurl_qq_1());
		values.setProfileUrl(null);
		values.setProviderUserId(userInfo.getOpenId());
	}

	@Override
	public UserProfile fetchUserProfile(QQ api) {
		return null;
	}

	@Override
	public void updateStatus(QQ api, String message) {
		//do noting
	}

}

public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {

	public QQConnectionFactory(String providerId, String appId, String appSecret) {
		super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
	}
}

微信登录

JWT替换默认的TOKEN

jwt 有什么特点呢

  • 自包含 (自定义它包含什么信息)
    • 不要存储密码信息哦
  • 签名密钥(SigningKey,这个很重要哦 安全性就靠它了,因为它是公开的标准)
  • 可扩展
  • 不像默认的TOKEN一样 ,它不需要存储
    • 默认的TOKEN是UUID生成的,且需要存储到数据库或者redis等
  • 优缺点
    • 优点是在分布式系统中,很好地解决了单点登录问题,很容易解决了session共享的问题。
    • 缺点
      • 无法作废已颁布的令牌/不易应对数据过期。
      • 如果将原存在服务端session中的各类信息都放在JWT中保存在客户端,可能造成JWT占用的空间变大,需要考虑cookie的空间限制等因素,如果放在Local Storage,则可能受到 XSS攻击。

长什么样子

// 3部分之间用“.”号做分隔。例如 :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 
  • header

    头部承载两部分信息

    • 声明类型,这里是jwt
    • 声明加密的算法 通常直接使用 HMAC SHA256
      JWT里验证和签名使用的算法,可选择下面的
    JWS 算法名称 描述
    HS256 HMAC256 HMAC with SHA-256
    HS384 HMAC384 HMAC with SHA-384
    HS512 HMAC512 HMAC with SHA-512
    RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256
    RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384
    RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512
    ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
    ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
    ES512 ECDSA512 ECDSA with curve P-521 and SHA-512
  • payload 自定义数据

    • 存放我们想放在token中存放的key-value值 即Claims
  • signature

    • 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分

推荐使用的java实现

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

生成和解析

// 生成token 

import com.sun.javafx.scene.traversal.Algorithm;
import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultJwsHeader;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


private String  SECRET = "SigningSecret_9527";
private void getJwtToken(){
      Date iatDate = new Date();
      // expire time
      Calendar expireTime = Calendar.getInstance();
      //有10天有效期
      expireTime.add(Calendar.DATE, 10);
      Date expiresDate = expireTime.getTime();
	  
      Claims claims = Jwts.claims();
      claims.put("name","test");
      claims.put("userId", "test");
      claims.setAudience("test");
      claims.setIssuer("test");
      String token = Jwts.builder().setClaims(claims).setExpiration(expiresDate)
              .signWith(SignatureAlgorithm.HS512, SECRET)
              .compact();

  }

/**
上面将token中的载荷放在chaims中,其实chaims是JWT内部维持的一个存放有效信息的地方,不论使用任何实现API,最终都使用chaims保存信息 。

setClaims() 有2个重载
 * JwtBuilder setClaims(Claims claims);
 * JwtBuilder setClaims(Map<String, Object> claims); 
 * 就是说,我们也可以直接传入map值对象。
*/

// 验证解析token
public void parseJwtToken(String token) {
        Jws<Claims> jws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
        String signature = jws.getSignature();
        Map<String, String> header = jws.getHeader();
        Claims Claims = jws.getBody();
 }


总结

  1. 在Web应用中,别再把JWT当做session使用,绝大多数情况下,传统的cookie-session机制工作得更好
  2. JWT适合一次性的命令认证,颁发一个有效期极短的JWT,即使暴露了危险也很小,由于每次操作都会生成新的JWT,因此也没必要保存JWT,真正实现无状态。
posted @ 2021-06-29 23:28  沉梦匠心  阅读(178)  评论(0)    收藏  举报