上一篇文章我们介绍了Security的原理并进行了源码的分析。本篇文章我们主要讲述如何实现,这一篇文章主要还是理论和思考性的东西,主要是带着大家思考和理解实现思路所包含的意义,抛砖引玉而已,大家可以触类旁通,去做更多的考量。
6.实现思路
针对上述流程,我们分别进行实现方式的说明:
6.1.登录接口
Secuirty有默认的登录接口地址/login
,当我们导入security的依赖后,就开始使用login作为登录的接口地址。如果用户在浏览器访问/login
,security就会认为你是在登录,就会进入到之前我们所分析的认证流程中去。这里有几点需要注意的事项:
- 登录路径必须是
/login
。这里可以在Security的配置类中自定义修改,配置loginProcessingUrl("/login")
。必须,permitAll()
要放行,不放行就会被Security拦截。 - 必须是
post
请求。这个也可以改,继承UsernamePasswordAuthenticationFilter
,实现自己的attemptAuthentication
方法即可。但一般谁会去把用户名密码放到请求路径上?所以没必要改post
请求方式。 - 必须使用
form
表单的方式提交,Content-Type
是multipart/form-data
。如果使用·json·传入,接收不到参数。可以改。如果你想要自定义兼容json
和form
表单两种数据提交方式,有两种方式:
- 继承
UsernamePasswordAuthenticationFilter
并重写attemptAuthentication
方法。
需要注意的是,你自定义的新过滤器不需要@Component
注入Spring容器,但必须在Security配置类中进行配置添加到过滤器链中,否则不生效。具体方法为配置addFilter(new CustomerAuthenticationFilter())
。
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//....此处省略其他无关配置
.addFilter(new CustomerAuthenticationFilter());
}
- 自定义一个登录接口,让请求直接进入到你的接口中,但是登录的后续操作还是要依赖于Security的一些组件,比如
AuthenticationManager
。你需要在Security的配置类中,创建AuthenticationManager的Bean
,并在你的具体service实现中注入它,然后后续的流程就还是UserDetailservice...
.。这里说的自定义登录接口和之前提过的自定义登录接口loginProcessingUrl
不是一回事:后者只是改了Security的登录请求路径;前者说的就是你和正常一样,写一个controller接口,让发出的请求进入这个接口中。
具体实现方法是:写一个controller接口,配置好路径;在Security配置类中配置上该接口,并设置为允许匿名访问:antMatchers("/oauth/login").anonymous()
。
在我个人看来,区别不大。但是新人看网络上的文章多了,就会被搞晕了,有这么做的还有那么做的,到底怎么做?我的建议就是找到一篇专业度比较高的大牛的文章,按照他的来一遍,先跑通了,知道怎么用了再说。千万不要看完这个觉得不错,再看那个也不错…,本来就是小白,再这么一搞,光停留在理论上了,可不就晕了?每个人的逻辑不一样,实际情况也不一样,你也不了解对方所处的背景情况,自然就也就无法理解,所以干就完了!!!触类旁通?那是后话,再说。
- 传入的参数名必须是username和password,否则拿不到数据。可以改,可以看到源码中
getParammeter()
的参数是一个usernameParameter
,这个可以在Security的配置类中进行自定义修改,配置usernameParameter("uname")
和passwordParameter("passwd")
。
参考示例:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 省略其他无关配置
// .antMatchers("/oauth/login").anonymous()
.and()
//启用表单身份验证
.formLogin()
.usernameParameter("uname")
.passwordParameter("passwd")
//设置进行登录请求处理的接口地址
.loginProcessingUrl("/auth/login")
.permitAll() // 和表单登录有关的直接放行
.and()
//注册自定义用户验证过滤器
.addFilter(new CustomerAuthenticationFilter(authenticationManager()));
}
6.2.获取用户信息
这里主要要说的是,我们需要根据自己的实际业务自定义从数据库获取用户密码、权限的方法,最终返回UserDetail对象,并将自定义的实现类配置在Security的配置中。我们有以下内容需要去做:
- 自定义
UserDetail
对象,重写里面的部分方法。
这里需要特别注意,如果我们并不使用UserDetail里面的几个状态字段,一定要把字段设置为true;如果能和我们的数据库字段对应上,那么可以直接把获取到的用户属性赋值给这几个字段。示例如下:
‼️构建自定义对象时,我们也可以将获取到的用户信息字段尽量多的放入,这样我们就有机会将经常要用的用户信息存入SpringContext上下文中,方便需要的时候直接从上下文中获取到足够使用的用户信息。
切记,密码等敏感信息不要放!
当用户信息被更新后,要及时更新上下文中的用户信息,避免引发不必要的问题。
public class SecurityUser extends User {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 用户ID
*/
@Getter
private Long id;
/**
* 部门ID
*/
@Getter
private String username;
/**
* 手机号
*/
@Getter
private String password;
/**
* 拓展字段:权限
*/
@Getter
private List<String> perms;
@JsonCreator
public SecurityUser (@JsonProperty("id") Long id, @JsonProperty("username") String username, @JsonProperty("password") String password,
@JsonProperty("enabled") boolean enabled, @JsonProperty("accountNonExpired") boolean accountNonExpired,
@JsonProperty("credentialsNonExpired") boolean credentialsNonExpired,
@JsonProperty("accountNonLocked") boolean accountNonLocked,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.id = id;
this.username = username;
this.password = password;
if (CollectionUtil.isNotEmpty(authorities)) {
this.perms = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
}
}
@JsonIgnore
@Override
public Collection<GrantedAuthority> getAuthorities() {
if (CollectionUtil.isNotEmpty(perms)) {
AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString(perms));
}
return null;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
- 自定义
UserDetailService
实现类,重写方法实现用户信息获取。示例如下:
@Service
public class SecurityUserServiceImpl implements UserDetailService {
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysRoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息(测试用,正式需从数据库根据用户名获取到用户信息)
SysUser user = userMapper.loadUserByUsername(username)
//获取用户权限信息
List<SysRole> roles = roleMapper.getUserRolesAndPerms(user.getId()))
//构建并返回自定义的UserDetail对象
return new SecurityUser(1L, username, user.getPassword(),
true, true, true, true,
AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString(roles));
}
}
6.3.密码解析
明文密码的处理:如果使用的是明文,那么被存储的密码前面需要加上{noop}
,否则系统会按照存储的是密文的方式进行校验。
如果我们使用密文,我们通常都是使用Bcrypt
加密算法,因此需要在Security中配置中创建BcrptPasswordEncoder的Bean
,并配置在认证管理器中。示例如下:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
}
6.4.生成Token
1. 为什么要使用token?
我目前要搭建的是分布式系统框架,前后端分离。在不分离的项目中,前后端都在一起,可以直接使用session和cookie,但分布式的系统,没有去搞session所有服务间共享的。既然不能共享session,那我们就需要一个办法让用户信息在资源服务间相互调用时进行传递,从而达到单点登录的目的。
2. 为什么要用Jwt来生成Token?
Jwt支持将一些自定义属性存放在里面,如此一来,资源服务获得其他服务的请求后可以通过反向解析Jwt生成的Token,获得用户信息。
3. Token的安全性如何保证?
Jwt支持使用加密key对数据进行签名加密。我们一般可以使用非对称加密算法如RSA,由认证服务器通过RSA密钥对中的私钥作为签名key加密数据,资源服务器使用RSA密钥对中的公钥对Token进行解密即可。
私钥不能对资源服务公开,防止有人伪造Token,因为私钥加密,公钥或者私钥都可以解密;但公钥加密,只有私钥才可以解密。资源服务只持有公钥,其他资源服务没有私钥是不能正确解密数据的,也就无法用伪造的数据骗到别人;而认证服务虽然有私钥,但它本身也不需要去接收和私钥解密资源服务发送的Token,我们也不提供这个逻辑。
另外,我们也可以使用相同的签名key来加密以及解密Token,但安全性较低。
实际运作中,我们一般会使用生成Jwt的工具类。参见如下:
6.4.1.导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<!--jackson包-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.9</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.1</version>
</dependency>
6.4.2.工具类
Jwt生成解析工具类
public class JwtUtils {
private static final String JWT_PAYLOAD_USER_KEY = "user";
私钥构建token //
/**
* 私钥加密token
*
* @param userInfo 载荷中的数据
* @param privateKey 私钥
* @param expire 过期时间,单位秒
* @return JWT
*/
public static String generateExpireTokenWithPriKey(Object userInfo, PrivateKey privateKey, int expire) {
return Jwts.builder()
.claim(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setId(createJti())
.setExpiration(DateTime.now().plusSeconds(expire).toDate())
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}
/**
* 公钥解析token
*
* @param token 用户请求中的token
* @param publicKey 公钥
* @return Jws<Claims>
*/
private static Jws<Claims> parserTokenWithPubKey(String token, PublicKey publicKey) {
return Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);
}
字符串构建token
/**
* 加密字符串生成token
*
* @param userInfo 载荷中的数据
* @param base64EncodedSecretKey base64编码后的加密字符
* @param expire 过期时间,单位秒
* @return JWT
*/
public static String generateExpireTokenWithSecretKey(Object userInfo, String base64EncodedSecretKey, int expire) {
return JWT.create()
.setPayload(JWT_PAYLOAD_USER_KEY, JsonUtils.toString(userInfo))
.setJWTId(createJti())
.setKey(base64EncodedSecretKey.getBytes())
.setExpiresAt(DateTime.now().plusSeconds(expire).toDate())
.sign();
}
/**
* 加密字符串解析token
*
* @param token 用户请求中的token
* @param base64EncodedSecretKey base64编码后的加密字符
* @return Jws<Claims>
*/
private static JWTPayload parserToken2PayloadWithSecretKey(String token, String base64EncodedSecretKey) {
return JWT.of(token).setKey(base64EncodedSecretKey.getBytes()).getPayload();
}
/**
* 加密字符串解析token
*
* @param token 用户请求中的token
* @param base64EncodedSecretKey base64编码后的加密字符
* @return Jws<Claims>
*/
private static String parserToken2ObjectWithSecretKey(String token, String base64EncodedSecretKey) {
return JWT.of(token).setKey(base64EncodedSecretKey.getBytes()).getPayload(JWT_PAYLOAD_USER_KEY).toString();
}
生成短令牌 //
/**
* 生成短令牌
*
* @return
*/
private static String createJti() {
return new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}
获取载荷信息 //
/**
* 获取token中的用户信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey, Class<T> userType) {
Jws<Claims> claimsJws = parserTokenWithPubKey(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setUserInfo(JsonUtils.toBean(body.get(JWT_PAYLOAD_USER_KEY).toString(), userType));
claims.setExpiration(body.getExpiration());
return claims;
}
/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @param publicKey 公钥
* @return 用户信息
*/
public static <T> Payload<T> getInfoFromToken(String token, PublicKey publicKey) {
Jws<Claims> claimsJws = parserTokenWithPubKey(token, publicKey);
Claims body = claimsJws.getBody();
Payload<T> claims = new Payload<>();
claims.setId(body.getId());
claims.setExpiration(body.getExpiration());
return claims;
}
}
RSA加解密工具类
public class RsaUtils {
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA";
private static final int DEFAULT_KEY_SIZE = 2048;
/**
* RSA密钥对对象
*/
public static class RsaKeyPair {
private final String publicKey;
private final String privateKey;
public RsaKeyPair(String publicKey, String privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey() {
return publicKey;
}
public String getPrivateKey() {
return privateKey;
}
}
密钥对 ///
/**
* 构建RSA密钥对
*
* @return 生成后的公私钥信息
*/
public static RsaKeyPair getRsaKeyPair() throws Exception {
KeyPair keyPair = getKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyStr = org.apache.commons.codec.binary.Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyStr = org.apache.commons.codec.binary.Base64.encodeBase64String(rsaPrivateKey.getEncoded());
return new RsaKeyPair(publicKeyStr, privateKeyStr);
}
/**
* 生成密钥对:密钥对中包含公钥和私钥
*
* @return 包含 RSA 公钥与私钥的 keyPair
*/
public static KeyPair getKeyPair() throws Exception {
// 获得RSA密钥对的生成器实例
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
// 安全随机数
SecureRandom secureRandom = new SecureRandom(String.valueOf(System.currentTimeMillis()).getBytes(CHARSET));
// 这里可以是1024、2048 初始化一个密钥对
keyPairGenerator.initialize(DEFAULT_KEY_SIZE, secureRandom);
// 获得密钥对
return keyPairGenerator.generateKeyPair();
}
/**
* 获取公钥 (并进行 Base64 编码,返回一个 Base64 编码后的字符串)
*
* @param keyPair:RSA 密钥对
* @return 返回一个 Base64 编码后的公钥字符串
*/
public static String getPublicKey(KeyPair keyPair) {
PublicKey publicKey = keyPair.getPublic();
byte[] bytes = publicKey.getEncoded();
return Base64.getEncoder().encodeToString(bytes);
}
/**
* 获取私钥(并进行Base64编码,返回一个 Base64 编码后的字符串)
*
* @param keyPair:RSA 密钥对
* @return 返回一个 Base64 编码后的私钥字符串
*/
public static String getPrivateKey(KeyPair keyPair) {
PrivateKey privateKey = keyPair.getPrivate();
byte[] bytes = privateKey.getEncoded();
return Base64.getEncoder().encodeToString(bytes);
}
加密文件 ///
/**
* 生成RSA密钥文件
*
* @return 生成后的公私钥信息
*/
public static void generateRsaKeyFiles(String publicKeyFilename, String privateKeyFilename) throws Exception {
KeyPair keyPair = getKeyPair();
// 获取公钥并写出
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyBytes = Base64.getEncoder().encode(publicKeyBytes);
writeFile(publicKeyFilename, publicKeyBytes);
// 获取私钥并写出
byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
privateKeyBytes = Base64.getEncoder().encode(privateKeyBytes);
writeFile(privateKeyFilename, privateKeyBytes);
}
/**
* 从文件中读取公钥
*
* @param filename 公钥保存路径,相对于classpath
* @return 公钥对象
* @throws Exception
*/
public static PublicKey getPublicKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPublicKey(bytes);
}
/**
* 从文件中读取密钥
*
* @param filename 私钥保存路径,相对于classpath
* @return 私钥对象
* @throws Exception
*/
public static PrivateKey getPrivateKey(String filename) throws Exception {
byte[] bytes = readFile(filename);
return getPrivateKey(bytes);
}
/**
* 获取公钥
*
* @param bytes 公钥的字节形式
* @return
* @throws Exception
*/
private static PublicKey getPublicKey(byte[] bytes) throws Exception {
bytes = Base64.getDecoder().decode(bytes);
X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
/**
* 获取密钥
*
* @param bytes 私钥的字节形式
* @return
* @throws Exception
*/
private static PrivateKey getPrivateKey(byte[] bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
bytes = Base64.getDecoder().decode(bytes);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(spec);
}
密钥/字符 转换 ///
/**
* 将 Base64 编码后的公钥转换成 PublicKey 对象
*
* @param pubStr:Base64 编码后的公钥字符串
* @return PublicKey
*/
public static PublicKey string2PublicKey(String pubStr) throws Exception {
byte[] bytes = Base64.getDecoder().decode(pubStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(bytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
return keyFactory.generatePublic(keySpec);
}
/**
* 将 Base64 码后的私钥转换成 PrivateKey 对象
*
* @param privateKeyStr:Base64 编码后的私钥字符串
* @return PrivateKey
*/
public static PrivateKey string2PrivateKey(String privateKeyStr) throws Exception {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr));
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
return keyFactory.generatePrivate(keySpec);
}
加密 ///
/**
* 公钥加密后再进行base64编码
*
* @param text 待加密的数据
* @param publicKeyStr 公钥
* @return 加密后的文本
*/
public static String publicKeyEncrypt(String text, String publicKeyStr) throws Exception {
return Base64.getEncoder().encodeToString(publicKeyEncrypt(text.getBytes(), publicKeyStr));
}
/**
* 公钥加密
*
* @param content 待加密的数据
* @param publicKeyStr 加密所需的公钥PublicKey
* @return 加密后的字节数组 byte[]
*/
public static byte[] publicKeyEncrypt(byte[] content, String publicKeyStr) throws Exception {
return publicKeyEncrypt(content, string2PublicKey(publicKeyStr));
}
/**
* 公钥加密
*
* @param content 待加密的数据
* @param publicKey 加密所需的公钥对象 PublicKey
* @return 加密后的字节数组 byte[]
*/
public static byte[] publicKeyEncrypt(byte[] content, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(content);
}
/**
* 私钥加密
*
* @param content 待加密的数据
* @param privateKeyStr 私钥
* @return 加密后的文本
*/
public static String privateKeyEncrypt(String content, String privateKeyStr) throws Exception {
return Base64.getEncoder().encodeToString((privateKeyEncrypt(content.getBytes(), privateKeyStr)));
}
/**
* 私钥加密
*
* @param content 待加密的数据
* @param privateKeyStr 私钥
* @return 加密后的文本
*/
public static byte[] privateKeyEncrypt(byte[] content, String privateKeyStr) throws Exception {
return privateKeyEncrypt(content, string2PrivateKey(privateKeyStr));
}
/**
* 私钥加密
*
* @param content 待加密的数据
* @param privateKey 私钥
* @return 加密后的内容
*/
public static byte[] privateKeyEncrypt(byte[] content, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(content);
}
解密 //
/**
* 公钥解密
*
* @param content 待解密的信息
* @param publicKeyStr 公钥
* @return 解密后的文本
*/
public static String publicKeyDecrypt(String content, String publicKeyStr) throws Exception {
return publicKeyDecrypt(Base64.getDecoder().decode(content), publicKeyStr);
}
/**
* 公钥解密
*
* @param content 待解密的信息
* @param publicKeyStr 公钥字符串
* @return 解密后的内容
*/
public static String publicKeyDecrypt(byte[] content, String publicKeyStr) throws Exception {
return publicKeyDecrypt(content, string2PublicKey(publicKeyStr));
}
/**
* 公钥解密
*
* @param content 待解密的信息
* @param publicKey 公钥
* @return 明文
*/
public static String publicKeyDecrypt(byte[] content, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(cipher.doFinal(content));
}
/**
* 私钥解密
*
* @param text 待解密的文本
* @param privateKeyStr 私钥
* @return 解密后的文本
*/
public static String privateKeyDecrypt(String text, String privateKeyStr) throws Exception {
return privateKeyDecrypt(Base64.getDecoder().decode(text), privateKeyStr);
}
/**
* 私钥解密
*
* @param content 待解密的文本
* @param privateKeyStr 私钥
* @return 解密后的文本
*/
public static String privateKeyDecrypt(byte[] content, String privateKeyStr) throws Exception {
PrivateKey privateKey = string2PrivateKey(privateKeyStr);
return new String(privateKeyDecrypt(content, privateKey));
}
/**
* 私钥解密
*
* @param content 待解密的内容 byte[],这里要注意,由于我们中间过程用的都是 BASE64 ,所以在传入参数前应先进行 BASE64 解析
* @param privateKey 解密需要的私钥对象 PrivateKey
* @return 解密后的字节数组 byte[],这里是元数据,需要根据情况自行转码
*/
public static byte[] privateKeyDecrypt(byte[] content, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(content);
}
文件操作 //
/**
* 从文件读取key
*
* @param fileName
* @return
* @throws Exception
*/
private static byte[] readFile(String fileName) throws Exception {
return Files.readAllBytes(new File(fileName).toPath());
}
/**
* 写出key文件
*
* @param destPath
* @param bytes
* @throws IOException
*/
private static void writeFile(String destPath, byte[] bytes) throws IOException {
File dest = new File(destPath);
if (!dest.exists()) {
dest.createNewFile();
}
Files.write(dest.toPath(), bytes);
}
}
6.5.校验/解析Token
6.5.1.网关验签,资源服务解析
前面已经说过,解析Token是在资源服务中要去做的,主要是为了获取里面自定义的用户信息。但其实还有一步在分布式系统中是需要做的,那就是在网关对token进行校验。总的来说,就是网关验签,资源服务解析
。
- 用户第一次登录成功后,我们会向用户响应一个Token,之后用户的每次请求都必须要携带着Token,资源服务得到后解析里面内容就知道是哪个用户了。网关的验签,只是对Token的真实性进行校验,防止人为伪造Token。因为我们使用了签名key来加密,所以前端用户是无法得到Token里面内容的,当然前提是签名key没有被泄露出去。
- Token的校验,可以直接调用Jwt工具类中的方法,传入对应的签名key即可。返回的Boolean值是true,网关继续后续操作就行了;false,那肯定就果断拒之门外了。
上面我们描述的
网关验签,资源服务解析
是一种常用的方法。
他的好处是如果由危险,直接在网关就被拦截,不会继续深入到认证或资源服务中;但他的坏处就是,在服务间发起Feign调用时,必须要想办法把Token放在请求头中,另一方收到请求再把信息解出来,其中涉及到需要自定义一个Feign调用的拦截器去做放置Token的工作。
6.5.2.第二种做法
在网关出做校验+解析
,把解析出来的信息以明文的方式放入请求头后再向下传递,这样资源服务就可以省区Token解析。但这种用法不多。
6.5.3.第三种做法
这种做法和权限校验有关。
- 当资源服务启动时将所有带有权限校验注解的方法的接口全部进行收集,可以存放在数据库或者redis缓存中,这样一来就有了所有的接口的权限列表;
- 当用户的请求到了网关后,拦截请求根据用户所请求的接口路径找出其对应的权限; 将用户Token进行解析,获取到当前用户的权限列表;
- 比对用户权限和请求路径所需要的权限,满足则放行,否则直接拒绝。这样一来,各个服务之间也就不需要相互传递Token了,只要网关校验一次即可。
- 但这里其实也会产生一些疑问,服务之间相互调用是不走网关的,Feign接口怎么判断权限呢?一般来说Feign是不需要加权限的,就像下订单一样,订单要去远程调用扣库存,难道扣库存的接口还要校验一遍用户有没有这个权限吗?如果你用的Feign接口是原有的正常对外的Controller接口,那你就这样改一下:重新定义一个新的Feign接口,不加权限注解,但和对外的接口调用同一个service方法。一般都不会建议混着用。
- 内部的调用一般都可以认为是安全的,毕竟如果不安全那意味着有人把你的Jar包给改了?那你要担心的重点可就不是Feign接口的安全了…你可以像一些框架一样,定义一个专门的标识,用来标识这个请求是内部调用,有标识直接放行即可;如果有人想从外部访问这个Feign接口,但没有标识,直接就拒绝提供服务。
6.6.权限校验
两种方式:一是使用权限注解控制;二是在Security配置文件中直接配置。
6.6.1.启用权限校验注解
注解的方式进行权限校验主要是由FilterSecurityInterceptor
拦截器来实现的。首先需要在Security配置类上增加注解@EnableGlobalMethodSecurity(prePostEnabled = true)
开启权限注解校验功能,然后在需要控制权限的方法上增加注解@PreAuthorize("hasRole('ROLE_ADMIN')")
,写上所需的权限即可。
从
EnableGlobalMethodSecurity
注解中,我们可以看到Security的权限控制注解支持方式有3种。prePostEnabled = true
是使用比较多的。
//支持spring表达式的注解
boolean prePostEnabled() default false;
//支持SpringSecurity提供的注解
boolean securedEnabled() default false;
//jsr250-api的注解,需要jsr250-api的jar包
boolean jsr250Enabled() default false;
6.6.2.开启方法上的权限校验
对应的注解也有多种,选择一种即可。@PreAuthorize是使用比较多的。
hasAuthority()方法是Spring的Spel表达式,实际是执行了SecurityExpressionRoot
的hasAuthority()
方法,方法中调用authentication
对象(也就是我们之前分析过的在authenticate()
方法最后调用createSuccessAuthentication()
封装的用户对象)的getAuthorities()方法获取用户的权限列表,然后判断参数是否在权限列表中。满足条件的就可以访问,否则没有权限。
hasAnyAuthority
方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
public String hello(){
return "hello";
}
hasRole
要求有对应的角色才可以访问。但是它内部会把我们传入的参数拼接上ROLE_
后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_
这个前缀才可以。
@PreAuthorize("hasRole('system:dept:list')")
public String hello(){
return "hello";
}
hasAnyRole
有任意的角色就可以访问。它内部也会把我们传入的参数拼接上ROLE_
后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_
这个前缀才可以。
@PreAuthorize("hasAnyRole('admin','system:dept:list')")
public String hello(){
return "hello";
}
6.6.3.自定义权限校验逻辑
这个知道就行,一般不需要去自定义。
6.6.3.1.自定义权限校验方法
@Component("ex")
public class SGExpressionRoot {
public boolean hasAuthority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判断用户权限集合中是否存在authority
return permissions.contains(authority);
}
}
6.6.3.2.在SPEL表达式中使用
@ex
相当于获取容器中bean的名字为ex
的对象,然后再调用这个对象的hasAuthority
方法。
@RequestMapping("/hello")
@PreAuthorize("@ex.hasAuthority('system:dept:list')")
public String hello(){
return "hello";
}
6.6.4.在配置文件中配置权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http
....
// 对于登录接口 允许匿名访问
.antMatchers("/testCors").hasAuthority("system:dept:list222")
....
}
6.6.5.扩展
实际上Spring Security提供了4个这样的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter
,@PreAuthorize
和@PostAuthorize
的作用分别是在方法调用前和调用后对权限进行检查,@PreFilter
和@PostFilter
的作用是对集合类的参数或返回值进行过滤。
//传入的id参数小于10
//传入的username=当前用户名
//传入的User对象的用户名=zhangsan
@PreAuthorize("#id<10 and principal.username.equals(#username) and #user.username.equals('zhangsan')")
//验证返回结果是否是偶数
@PostAuthorize("returnObject%2==0")
@RequestMapping("/test1")
public Integer test1(Integer id, String username, User user) {
return id;
}
//过滤传入的参数保留偶数
@PreFilter("filterObject%2==0")
//过滤返回结果保留被4整除的数
@PostFilter("filterObject%4==0")
@RequestMapping("/test2")
public List<Integer> test2(List<Integer> idList) {
return idList;