一点微小的积累
Spring Security提供了一整套的用户授权鉴权机制,在Spring全家桶中使用起来也十分方便。
首先添加一个security
的配置文件
这个自定义的SecurityJavaConfig
需要继承WebSecurityConfigurerAdapter
类,并重写configure
方法,在configure
方法中实现对请求的过滤和校验。
addFilter**
即在指定的位置添加自己定义的filter:
我这里定义了三个filter,LoginAuthenticationFilter
用来处理登录,JwtAuthenticationFilter
处理jwt校验,WxAuthenticationFilter
处理微信相关的token校验。
LoginAuthenticationFilter
定义。主要就是读取请求参数里的username
和password
,可以直接调用默认的UsernamePasswordAuthenticationFilter
校验,基本不需要做太多自定义的事。:
package com.moicen.spring.rest.filter;
import com.moicen.spring.rest.common.AuthenticationBean;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public void afterPropertiesSet() {
Assert.notNull(getAuthenticationManager(), "authenticationManager must be specified");
Assert.notNull(getSuccessHandler(), "AuthenticationSuccessHandler must be specified");
Assert.notNull(getFailureHandler(), "AuthenticationFailureHandler must be specified");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authenticationToken = null;
try {
AuthenticationBean authenticationBean = mapper.readValue(getRequestPostStr(request), AuthenticationBean.class);
authenticationToken = new UsernamePasswordAuthenticationToken(
authenticationBean.getUsername(), authenticationBean.getPassword());
} catch (IOException e) {
authenticationToken = new UsernamePasswordAuthenticationToken(null, null);
}
setDetails(request, authenticationToken);
return this.getAuthenticationManager().authenticate(authenticationToken);
}
private static byte[] getRequestPostBytes(HttpServletRequest request)
throws IOException {
int contentLength = request.getContentLength();
if (contentLength < 0) {
return new byte[0];
}
byte[] buffer = new byte[contentLength];
int i = 0;
while (i < contentLength) {
int cursor = request.getInputStream().read(buffer, i, contentLength - i);
if (cursor == -1) {
break;
}
i += cursor;
}
return buffer;
}
private static String getRequestPostStr(HttpServletRequest request)
throws IOException {
byte[] buffer = getRequestPostBytes(request);
String charEncoding = request.getCharacterEncoding();
if (charEncoding == null) {
charEncoding = "UTF-8";
}
return new String(buffer, charEncoding);
}
}
JwtAuthenticationFilter
这里使用@Value
从配置文件resources/application.yaml
中读取jwt
相关的配置。
重写doFilterInternal
方法来做校验,主要就是通过jwt
读取userId
,然后从数据库读取用户信息,然后继续重用UsernamePasswordAuthenticationToken
来保存UserDetails
。注意这里我使用了自定义的UserDetailsService
,主要是Spring Security
自带的UserDetailsService
不太顺手。一般的系统里都会有自己定义的一套权限系统,跟Spring Security默认的结构不太一样。
@Service
public class WebUserDetailsService {
@Autowired
private UserService userService;
public OcrUser loadUserByUsername(String username) {
OcrUser user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username + " not found");
}
return user;
}
}
同时另外定义了一个WebAuthenticationProvider
类,用于做实际的鉴权操作:
注意这里因为密码加密时使用了随机的salt
,因此每次生成的密码是不一样的,不能直接做相等比较,需要使用encoder.matches
方法来对比。
AuthenticationManager
使用ProviderManager
来管理和使用provider,AuthenticationProvider
需要重写supports
方法来表明自己支持哪一种Authentication
在自定义的SecurityJavaConfig
中配置使用上述provider:
permissiveRequest
和setPermissiveUrl
可以用来设置规则让不需要校验的请求直接通过。这个是使用正则表达式对URL做匹配,对简单的规则比较方便,如果规则比较复杂那么写起来就很麻烦。因此这里使用另一种更简单灵活的方式:重写shouldNotFilter
方法,在这里可以做任何方式的校验,只要最终返回一个boolean
就可以了,比如我这里设置包含/wx/
的路由都不需要走这个校验。
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI();
return uri.contains("/wx/");
}
WxAuthenticationFilter
。因为我这里跟微信交互的比较多,而且校验方式不一样,因此额外写了一个专门处理微信相关请求的filter。整体跟JwtAuthenticationFilter
基本一致,只是shouldNotFilter
正好相反,另外从jwt
读取用户信息时做了额外判断。接下来定义一个JwtUtil
类来专门处理jwt
相关
最后测试以上filter
LoginAuthenticationFilter
@Test
public void loginTest() throws Exception {
Response response = given().contentType(ContentType.JSON)
.body("{\"username\": \"bar_user\", \"password\": \"bar_pwd\"}")
.post("/login");
ObjectMapper objectMapper = new ObjectMapper();
HttpWebResponse res = objectMapper.readValue(response.asString(), HttpWebResponse.class);
System.out.println("----------token: " + res.getResult());
assertTrue(res.isSuccess());
assertEquals("bar_user", JwtUtil.getIdentifier(res.getResult().toString()));
}
JwtAuthenticationFilter
@Test
public void jwtTest() throws Exception {
String token = "eyJhbGciOiJIUzI1NiIsImN0eSI6ImFwcGxpY2F0aW9uL2pzb24ifQ==.eyJpZGVudGlmaWVyIjoiYmFyX3VzZXIiLCJ0aW1lc3RhbXAiOjE1Njc2OTM0OTg5NjMsInR5cGUiOiJVU0VSTkFNRSJ9.lmZUE2XwVVqF4zdimrI8fVP6wEosxdsgqxYZQuDBh3c=";
Response response = given()
.header("Authorization", "Bearer " + token)
.contentType(ContentType.JSON)
.get("/users/current");
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("----------------response: " + response.asString());
HttpWebResponse res = objectMapper.readValue(response.asString(), HttpWebResponse.class);
assertTrue(res.isSuccess());
assertNotNull(res.getResult());
SpringUser user = objectMapper.convertValue(res.getResult(), SpringUser.class);
assertEquals("bar_user", user.getUsername());
assertEquals("bar", user.getOpenid());
}
WxAuthenticationFilter
@Test
public void jwtOpenidTest() throws Exception {
String token = JwtUtil.encode("foo");
Response response = given()
.header("Authorization", "Bearer " + token)
.contentType(ContentType.JSON)
.get("/photos/wx/query");
System.out.println("-----------response: " + response.asString());
HttpWebResponse res = objectMapper.readValue(response.asString(), HttpWebResponse.class);
assertTrue(res.isSuccess());
}