resulful规范_Spring security前后端分离Json认证 跨域自定义Login Logout filter

引言

前端Vue采用Json登陆,通常是跨域的,而且由于是Resulful无状态的请求,认证后的状态都是靠后台发出的token保持。默认的Spring security认证完成后会进行302重定向,这显然不符合我们的要求。登陆成功后我们想返回用户的相关信息,也是Json,这就需要自定义配置。再加上token的处理。。

好在Spring security全部都可以自定义,只是网上都是Sringboot的配置,这次项目使用的繁琐的SpringMVC的xml配置,摸索了好久终于搞定。

1. 明确默认的Spring sercurity filter chain

自定义Spring sercurity,基本上就是自定义各种filter chain。以下是filter chain的顺序跟别名。具体可以查看官网。

这里明确以下我们需要替换的filter。

AliasFilter ClassNamespace Element or Attribute

CHANNEL_FILTER

ChannelProcessingFilter

http/intercept-url@requires-channel

SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter

http

CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter

session-management/concurrency-control

HEADERS_FILTER

HeaderWriterFilter

http/headers

CSRF_FILTER

CsrfFilter

http/csrf

LOGOUT_FILTER

LogoutFilter

http/logout

X509_FILTER

X509AuthenticationFilter

http/x509

PRE_AUTH_FILTER

AbstractPreAuthenticatedProcessingFilterSubclasses

N/A

CAS_FILTER

CasAuthenticationFilter

N/A

FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter

http/form-login

BASIC_AUTH_FILTER

BasicAuthenticationFilter

http/http-basic

SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter

http/@servlet-api-provision

JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

http/@jaas-api-provision

REMEMBER_ME_FILTER

RememberMeAuthenticationFilter

http/remember-me

ANONYMOUS_FILTER

AnonymousAuthenticationFilter

http/anonymous

SESSION_MANAGEMENT_FILTER

SessionManagementFilter

session-management

EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter

http

FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor

http

SWITCH_USER_FILTER

SwitchUserFilter

N/A

2. 自定义FORM_LOGIN_FILTER

首先就是FORM_LOGIN_FILTER,默认使用UsernamePasswordAuthenticationFilter,xml配置为http/form-login。该filter取得用户名跟密码的方法,就是单纯的调用request.getParameter(),这样是无法取得Json形式的用户名跟密码的。

在这里我们创建继承UsernamePasswordAuthenticationFilter的自定义Login filter。

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.InputStream;

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter{

@Override

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)

throws AuthenticationException{

ObjectMapper mapper = new ObjectMapper();

UsernamePasswordAuthenticationToken token;

try (InputStream is = request.getInputStream()) {

// 从Json中获取用户名,密码

MAccount mAccount = mapper.readValue(is, MAccount.class);

token = new UsernamePasswordAuthenticationToken(mAccount.getLoginName(), mAccount.getLoginPassword());

} catch (IOException e) {

throw new BadCredentialsException(MessageUtil.getMessage("MSG_LOGIN_FAILURE"));

}

setDetails(request, token);

return this.getAuthenticationManager().authenticate(token);

}

}

复制代码

3. 自定义AuthenticationProvider

取到用户信息之后,真正进行验证的是AuthenticationProvider。这里同样需要自定义,通过一个取数据库信息的UserDetailsService,然后进行验证比对。验证失败则生成一个BadCredentialsException,父类会帮我们处理。

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import java.nio.charset.StandardCharsets;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.Optional;

public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider{

private UserDetailsService userDetailsService;

@Override

protected void additionalAuthenticationChecks(UserDetails userDetails,

UsernamePasswordAuthenticationToken authentication) throws AuthenticationException{

String pw = authentication.getCredentials().toString();

Optional.ofNullable(pw).orElseThrow(() -> new BadCredentialsException("password null"));

String pwDigest;

// 对密码加密后验证

try {

MessageDigest md = MessageDigest.getInstance("SHA-256");

md.update(pw.getBytes(StandardCharsets.UTF_8));

pwDigest = StringUtils.byte2Hex(md.digest());

} catch (NoSuchAlgorithmException e) {

throw new BadCredentialsException(e.getMessage());

}

// 验证密码

if (!pwDigest.equals(userDetails.getPassword())) {

throw new BadCredentialsException("password does not match");

}

}

@Override

protected UserDetails retrieveUser(String username,

UsernamePasswordAuthenticationToken authentication)

throws AuthenticationException{

return userDetailsService.loadUserByUsername(username);

}

public void setUserDetailsService(UserDetailsService userDetailsService){

this.userDetailsService = userDetailsService;

}

}

复制代码

4. 自定义UserDetailsService

刚才提到的UserDetailsService,要重写loadUserByUsername方法,以达到的访问数据库用户信息的目的。

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.ArrayList;

import java.util.Optional;

public class CustomUserDetailsService implements UserDetailsService{

@Autowired

MAccountDao dao;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{

QueryWrapper queryWrapper = new QueryWrapper<>();

queryWrapper.eq("login_name", username);

// 通过自己的dao获取数据库信息

MAccount mAccount = dao.selectOne(queryWrapper);

Optional.ofNullable(mAccount).orElseThrow(() -> new BadCredentialsException("LOGIN_FAILURE"));

ArrayList list = new ArrayList<>();

// 添加权限

list.add(new SimpleGrantedAuthority(String.valueOf(mAccount.getRoleType())));

return new User(username, mAccount.getLoginPassword(), list);

}

}

复制代码

5. 自定义AuthenticationSuccessHandler跟AuthenticationFailureHandler

开头说了,认证成功后者失败后,不进行重定向跳转,而是返回Json数据,这就需要自定义Handler。

AuthenticationSuccessHandler

认证成功后做两件事,第一件是生成token,之后的请求不再需要用户名密码,每次验证token。第二件是把信息返回给前端。

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.http.HttpStatus;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import static jp.co.saftec.siled.auth.JWTAuthenticationFilter.HEADER_STRING;

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{

@Autowired

private UserDetailsService userDetailsService;

@Autowired

MAccountDao dao;

@Override

public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) throws IOException{

// UserDetails取得

UserDetails userDetails = userDetailsService.loadUserByUsername(auth.getName());

// 生成token

JwtTokenUtil jwtTokenUtil = new JwtTokenUtil();

String token = jwtTokenUtil.generateToken(userDetails);

// 设置跨域

res.setHeader("Access-Control-Allow-Origin", "*");

res.setHeader(HEADER_STRING, token);

// 返回前端Json

ResponseUtil.makeJsonResponse(res, HttpStatus.OK.value(), dao.selectAccountInfo(auth.getName()));

}

}

复制代码AuthenticationFailureHandler

认证失败比较简单,直接返回403。

import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;

import org.springframework.http.HttpStatus;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler{

// logger

private Logger logger = LogManager.getLogger(this.getClass());

@Override

public void onAuthenticationFailure(

HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException{

if (e instanceof BadCredentialsException) {

logger.warn("loginFailure");

}

ErrorResponseDto dto = new ErrorResponseDto();

dto.setErrorMessage(e.getMessage());

ResponseUtil.makeJsonResponse(response, HttpStatus.FORBIDDEN.value(),dto);

}

}

复制代码ResponseUtil

用于生成Json的response。

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.PrintWriter;

public class ResponseUtil{

public static void makeJsonResponse(HttpServletResponse response, Integer httpStatus, Object JsonObject) throws IOException{

response.setStatus(httpStatus);

response.setContentType("application/json; charset=utf-8");

ObjectMapper objectMapper = new ObjectMapper();

PrintWriter out = response.getWriter();

out.write(objectMapper.writeValueAsString(JsonObject));

out.flush();

out.close();

}

}

复制代码JwtTokenUtil

token工具类。用于生成,验证token过期等。

import io.jsonwebtoken.Claims;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.SignatureAlgorithm;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.stereotype.Component;

import java.time.Instant;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

@Component

public class JwtTokenUtil{

private static final String CLAIM_KEY_USERNAME = "sub";

private static final long EXPIRATION_TIME = 432000000;

private static final String SECRET = "secret";

public String generateToken(UserDetails userDetails){

Map claims = new HashMap<>(16);

claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());

return Jwts.builder()

.setClaims(claims)

.setExpiration(new Date(Instant.now().toEpochMilli() + EXPIRATION_TIME))

.signWith(SignatureAlgorithm.HS512, SECRET)

.compact();

}

public Boolean validateToken(String token, UserDetails userDetails){

User user = (User) userDetails;

String username = getUsernameFromToken(token);

return (username.equals(user.getUsername()) && !isTokenExpired(token));

}

public Boolean isTokenExpired(String token){

Date expiration = getExpirationDateFromToken(token);

return expiration.before(new Date());

}

public String getUsernameFromToken(String token){

return getClaimsFromToken(token).getSubject();

}

public Date getExpirationDateFromToken(String token){

return getClaimsFromToken(token).getExpiration();

}

private Claims getClaimsFromToken(String token){

return Jwts.parser()

.setSigningKey(SECRET)

.parseClaimsJws(token)

.getBody();

}

}

复制代码

6. 自定义OncePerRequestFilter

搞定了Login认证后,接下来要解决的事情是登陆成功之后的请求都是通过token验证,而不再发送用户名跟密码,验证token信息是否正确是否过期的则是自定义的OncePerRequestFilter。

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import org.springframework.security.core.context.SecurityContextHolder;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

public class JWTAuthenticationFilter extends OncePerRequestFilter{

/**

* token header(这个是你跟前端商量好的请求头,不一定是我这个)

*/

public static final String HEADER_STRING = "X-Session-Token";

@Autowired

private UserDetailsService userDetailsService;

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException{

String token = request.getHeader(HEADER_STRING);

// token解析

if(StringUtils.isNotBlank(token)) {

JwtTokenUtil jwtTokenUtil = new JwtTokenUtil();

String userName = jwtTokenUtil.getUsernameFromToken(token);

// token解析成功付与権限

if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {

UserDetails userDetails = userDetailsService.loadUserByUsername(userName);

// 验证token是否有効期限内

if (jwtTokenUtil.validateToken(token, userDetails)) {

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(

userDetails, null, userDetails.getAuthorities());

authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(

request));

// 権限付与

SecurityContextHolder.getContext().setAuthentication(authentication);

}

}

}

// filterChain继续执行

filterChain.doFilter(request, response);

}

}

复制代码

7. 自定义AuthenticationEntryPoint

匿名访问时候,默认情况下登陆失败会跳转页面,这里就简单的返回401。

import org.springframework.http.HttpStatus;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

public class UnauthorizedEntryPoint implements AuthenticationEntryPoint{

@Override

public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException{

// 401を返す

ErrorResponseDto dto = new ErrorResponseDto();

dto.setErrorMessage(authException.getMessage());

ResponseUtil.makeJsonResponse(response, HttpStatus.UNAUTHORIZED.value(), dto);

}

}

复制代码

8. 把以上配置添加到xml

这里就要羡慕使用Springboot的同学了,xml简直就是地狱。

spring-security.xml

xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd

http://www.springframework.org/schema/security

https://www.springframework.org/schema/security/spring-security.xsd">

*

*

GET

POST

PUT

DELETE

TRACE

OPTIONS

class="jp.co.saftec.siled.auth.UnauthorizedEntryPoint">

class="jp.co.saftec.siled.auth.MyAuthenticationFailureHandler">

复制代码web.xml

(省略无关部分)

contextConfigLocation

/WEB-INF/spring/root-context.xml,

/WEB-INF/spring/spring-security.xml

springSecurityFilterChain

org.springframework.web.filter.DelegatingFilterProxy

springSecurityFilterChain

/*

复制代码

9. 自定义Logout filter

经过九九八十一难,终于把登陆搞定了,如果只需要自定义登陆,下面的就不用看了。以下是自定义Logout的配置,作为bean在上面的xml中已经配置过了,这里贴出java。

MyLogoutSuccessHandler

注销成功,则返回200。

import org.springframework.http.HttpStatus;

import org.springframework.security.core.Authentication;

import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

public class MyLogoutSuccessHandler implements LogoutSuccessHandler{

@Override

public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{

ResponseUtil.makeJsonResponse(response, HttpStatus.OK.value(),"Logout OK");

}

}

复制代码TokenClearHeaderWriter

注销时前端只发送给后端token,把token清除后就算是注销成功了。这里是回调方法,被xml中注册的LogoutFilter调用。时机是/logout被请求时。

public class TokenClearHeaderWriter implements HeaderWriter{

@Override

public void writeHeaders(HttpServletRequest request, HttpServletResponse response){

String token = request.getHeader(HEADER_STRING);

// token清除

if(StringUtils.isNotBlank(token)) {

response.setHeader(HEADER_STRING, "");

}

// 真正的权限清除

SecurityContextHolder.getContext().setAuthentication(null);

}

}

复制代码

最后

到这里算是终于搞定了自定义Json认证。xml的部分之外是跟springboot没有区别的,但xml是真的很繁琐,错一个地方都无法启动。建议没事的调试以下Spring security的源码,理解了源码配置才能有理有据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值