最近在接触spring security,它是个好东西,只是对Restful的支持并不是那么友好。对于习惯使用Restful接口的我,还是选择把spring security认证功能与RememberMe功能所用到的某些类进行重写,以实现spring security对Restful支持。
重写认证功能
1. 重写UsernamePasswordAuthenticationFilter
package com.tinyhelp.security;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tinyhelp.security.AuthenticationBean;
public class TinyHelpUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
@SuppressWarnings("finally")
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if(request != null && request.getMethod().equalsIgnoreCase("POST") &&
request.getContentType() != null &&
(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE) ||
request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE))) {
//当认证请求是JSON
//使用fastjson转换
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()){
//忽略未知属性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//读到认证Bean
AuthenticationBean authenticationBean = mapper.readValue(is,AuthenticationBean.class);
//生成认证令牌
authRequest = new UsernamePasswordAuthenticationToken(
authenticationBean.getPhoneNumber(), authenticationBean.getPassword());
}catch (IOException e) {
e.printStackTrace();
}finally {
//配置将request,authRequest注入到认证请求详情的属性中
setDetails(request, authRequest);
//返回认证后的身份
return this.getAuthenticationManager().authenticate(authRequest);
}
}
//否则非Json请求,使用原本的认证方式
return super.attemptAuthentication(request, response);
}
}
package com.tinyhelp.security;
/**
* 自定义认证Bean
*/
public class AuthenticationBean {
private String phoneNumber;
private String password;
private String rememberMe;
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRememberMe() {
return rememberMe;
}
public void setRememberMe(String rememberMe) {
this.rememberMe = rememberMe;
}
}
2. 将重写了的Filter在configure(HttpSecurity http)中配置
//自定义用户认证filter与原来的filter放在相同位置,但不覆盖原来的filter
http
.addFilterAt(tinyHelpUsernamePasswordAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
@Bean
TinyHelpUsernamePasswordAuthenticationFilter tinyHelpUsernamePasswordAuthenticationFilter() throws Exception {
TinyHelpUsernamePasswordAuthenticationFilter filter = new TinyHelpUsernamePasswordAuthenticationFilter();
//只接受post请求
filter.setPostOnly(true);
//设置自定义认证成功处理类
filter.setAuthenticationSuccessHandler(new TinyHelpAuthenticationSuccessHandler());
//设置自定义认证失败处理类
filter.setAuthenticationFailureHandler(new TinyHelpAuthenticationFailureHandler());
//设置自定义认证处理url
filter.setFilterProcessesUrl("/account/login");
//设置自定义RememberMe功能
filter.setRememberMeServices(rememberMeServices());
//重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
package com.tinyhelp.security;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tinyhelp.entity.Msg;
public class TinyHelpAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//设置为普通文本响应,但文本为json格式
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
// 成功,跳转首页
Msg msg = new Msg();
msg.setCode(0);
msg.setMsg("/index.html");
// 输出Json
out.write(new ObjectMapper().writeValueAsString(msg));
out.flush();
out.close();
}
}
package com.tinyhelp.security;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tinyhelp.entity.Msg;
public class TinyHelpAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
//设置为普通文本响应,但文本为json格式
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
// 失败
Msg msg = new Msg();
if(exception.getMessage().equals("用户不存在")) {
msg.setCode(16);
msg.setMsg("手机号不存在");
}else if(exception.getMessage().equals("认证信息没有手机号")){
msg.setCode(500);
msg.setMsg("认证信息没有手机号");
}else{
msg.setCode(16);
msg.setMsg("密码错误");
}
out.write(new ObjectMapper().writeValueAsString(msg));
out.flush();
out.close();
}
}
@Bean
public RememberMeServices rememberMeServices(){
TinyHelpTokenBasedRememberMeServices tokenBasedRememberMeServices = new TinyHelpTokenBasedRememberMeServices(
REMEMBER_ME_KEY, new TinyHelpUserDetailsService(userService));
//cookie名
tokenBasedRememberMeServices.setCookieName(REMEMBER_ME_COOKIE_NAME);
//cookie时间
tokenBasedRememberMeServices.setTokenValiditySeconds(REMEMBER_ME_TOKEN_TIME_OUT);
return tokenBasedRememberMeServices;
}
private static final String REMEMBER_ME_COOKIE_NAME = "rememberMe"; //cookie名
private static final int REMEMBER_ME_TOKEN_TIME_OUT = 2419200; //token四周内有效
private static final String REMEMBER_ME_KEY = "TinyHelpSecured"; //私钥名
3. 自定义的daoAuthenticationProvider,并在configure(AuthenticationManagerBuilder auth)中配置
auth
.authenticationProvider(daoAuthenticationProvider()); //使用自定义的daoAuthenticationProvider
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
//自定义数据库认证提供者
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//不隐藏UserNotFoundExceptions
provider.setHideUserNotFoundExceptions(false);
//使用自定义用户详情服务类
provider.setUserDetailsService(new TinyHelpUserDetailsService(userService));
//使用自定义MD5编码器
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Autowired
private UserService userService;
package com.tinyhelp.security;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.tinyhelp.entity.User;
import com.tinyhelp.service.UserService;
public class TinyHelpUserDetailsService implements UserDetailsService {
private final UserService userService; //从外部赋予
public TinyHelpUserDetailsService(UserService userService) {
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException {
if(phoneNumber == null) {
throw new UsernameNotFoundException("认证信息没有手机号");
}
//通过手机号找用户
User user = userService.findUserByPhone(phoneNumber);
if (user != null) {
//创建权限列表
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
//返回用户详情,包括手机号,密码和权限
return new org.springframework.security.core.userdetails.User(
user.getPhoneNumber(),
user.getPassword(),
authorities);
}
throw new UsernameNotFoundException("用户不存在");
}
}
@Bean
PasswordEncoder passwordEncoder() {
//自定义MD5编码器
return new MD5PasswordEncoder();
}
重写RememberMe功能
1. 重写TokenBasedRememberMeServices
package com.tinyhelp.security;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TinyHelpTokenBasedRememberMeServices extends TokenBasedRememberMeServices{
private boolean alwaysRemember;
public void setAlwaysRemember(boolean alwaysRemember) {
this.alwaysRemember = alwaysRemember;
}
public TinyHelpTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService) {
super(key, userDetailsService);
}
@Override
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (alwaysRemember) {
return true;
}
if(request != null && request.getMethod().equalsIgnoreCase("POST") &&
request.getContentType() != null &&
(request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE) ||
request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE))) {
//当认证请求是JSON
//使用fastjson转换
ObjectMapper mapper = new ObjectMapper();
try (InputStream is = request.getInputStream()) {
//忽略未知属性
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//remember-me读到认证Bean
AuthenticationBean authenticationBean = mapper.readValue(is,AuthenticationBean.class);
//如果请求中有remember-me 字段并且值为on则返回true
if(authenticationBean.getRememberMe() != null &&
authenticationBean.getRememberMe().equalsIgnoreCase("on")){
return true;
}
}catch (IOException e) {
e.printStackTrace();
}
}
//否则调用原本的自我记住功能
return super.rememberMeRequested(request, parameter);
}
}
2. 组建RememberMeAuthenticationFilter,并在configure(HttpSecurity http)中配置
//自定义记住我filter与原来的filter放在相同位置,但不覆盖原来的filter
http
.addFilterAt(rememberMeAuthenticationFilter(),
RememberMeAuthenticationFilter.class);
@Bean
public RememberMeAuthenticationFilter rememberMeAuthenticationFilter() throws Exception {
//重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
return new RememberMeAuthenticationFilter(authenticationManagerBean(), rememberMeServices());
}
@Bean
public RememberMeServices rememberMeServices(){
TinyHelpTokenBasedRememberMeServices tokenBasedRememberMeServices = new TinyHelpTokenBasedRememberMeServices(
REMEMBER_ME_KEY, new TinyHelpUserDetailsService(userService));
//cookie名
tokenBasedRememberMeServices.setCookieName(REMEMBER_ME_COOKIE_NAME);
//cookie时间
tokenBasedRememberMeServices.setTokenValiditySeconds(REMEMBER_ME_TOKEN_TIME_OUT);
return tokenBasedRememberMeServices;
}
3.自定义的rememberMeAuthenticationProvider,并在configure(AuthenticationManagerBuilder auth)中配置
auth
.authenticationProvider(rememberMeAuthenticationProvider()); //使用自定义的rememberMeAuthenticationProvider
@Bean
public RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
//自定义记住我提供者
return new RememberMeAuthenticationProvider(REMEMBER_ME_KEY);
}
包装Request
我知道你已经很累,但这是最后一关。细心的你可能已经发现代码里要获取两遍request的inputStream流,但事实真能获取两遍吗。如果你没有包装request,第二次获取的时候会为空。因为request只能读取一次。
原因参见:https://blog.csdn.net/zj20142213/article/details/80012221
实现:
//包装request的filter,放在spring security UsernamePasswordAuthenticationFilter之前
http
.addFilterBefore(new WrapRequestFilter(),
UsernamePasswordAuthenticationFilter.class);
package com.tinyhelp.security;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.MediaType;
import org.springframework.web.filter.GenericFilterBean;
import com.tinyhelp.tool.TinyHelpHttpServletRequestWrapper;
/**
* 用于包装request,
* 以便实现对request inputStream的多次读取
*/
public class WrapRequestFilter extends GenericFilterBean{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = null;
if(request instanceof HttpServletRequest){
httpRequest = (HttpServletRequest) request;
}
//判断request是否是POST请求并且是json请求
if(httpRequest != null && httpRequest.getMethod().equalsIgnoreCase("POST") &&
httpRequest.getContentType() != null &&
(httpRequest.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE) ||
httpRequest.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE))){
//是的话,包装request请求
request = new TinyHelpHttpServletRequestWrapper(httpRequest);
}
chain.doFilter(request, response);
}
}
package com.tinyhelp.tool;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class TinyHelpHttpServletRequestWrapper extends HttpServletRequestWrapper {
private HttpServletRequest request;
private String requestBody = null;
public TinyHelpHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
if (requestBody == null) {
requestBody = readBody(this.request);
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CustomServletInputStream(requestBody);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private static String readBody(ServletRequest request) {
StringBuilder sb = new StringBuilder();
String inputLine;
BufferedReader br = null;
try {
br = request.getReader();
while ((inputLine = br.readLine()) != null) {
sb.append(inputLine);
}
} catch (IOException e) {
throw new RuntimeException("Failed to read body.", e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
}
}
}
return sb.toString();
}
private class CustomServletInputStream extends ServletInputStream {
private ByteArrayInputStream buffer;
public CustomServletInputStream(String body) {
body = body == null ? "" : body;
this.buffer = new ByteArrayInputStream(body.getBytes());
}
@Override
public int read() throws IOException {
return buffer.read();
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
}
以上代码中有注释,但最好还是深入理解一下spring security运行机制,你将会获得更多收获。楼主重写这些的时候,也是这么过来的。
至于原理,推荐以下博文:
https://blog.csdn.net/liushangzaibeijing/article/details/81220610#