2021SC@SDUSC
概述在之前的所有分析中,我们分析了项目的配置和架构情况,并且根据后端的MVC架构,在不同的层级,对spring boot框架本身以及注解的使用方法和内部原理进行了着重分析,希望能加深对spring boot框架机制的理解情况。
在后面的分析里,我们将把分析的重点放在项目所使用的其他技术上,比如jwt,spring security,rabbitmq,nginx等等,重点讲解其内部原理,附带项目配置和使用方法。
在本次的博客里,我们将重点先放在项目的身份校验与权限控制上,技术上使用了jwt+spring security+filter来实现。
基本配置可参考:
Spring Security基本配置 - 仅此而已-远方 - 博客园
以及我项目组成员的具体介绍:
Spring Security+JWT实现身份认证与权限控制_alphahao的博客-CSDN博客
后者有在本项目的详细配置,不再赘述。
原理分析 身份校验Spring Security的登录验证流程核心是过滤器链(Filter Chain)。
当一个请求到达时按照过滤器链的顺序依次进行处理,通过所有过滤器链的验证的请求才能访问接口。
同时,在验证的同时进行授权,方便之后的权限管理。
Spring Security的封装程度较高,我们使用时只需要调用其实现的方法就可以实现相关功能。
SpringSecurity提供了多种登录认证的方式,由多种Filter过滤器来实现,比如:
- BasicAuthenticationFilter实现的是HttpBasic模式的登录认证
- UsernamePasswordAuthenticationFilter实现用户名密码的登录认证
- RememberMeAuthenticationFilter实现登录认证的“记住我”的功能
- SmsCodeAuthenticationFilter实现短信验证码登录认证
- SocialAuthenticationFilter实现社交媒体方式登录认证的处理
- Oauth2AuthenticationProcessingFilter和Oauth2ClientAuthenticationProcessingFilter实现Oauth2的鉴权方式
以用户名密码的登录认证为例的流程如下:
security config源码:
spring security的全局配置都在这里,在此方法中添加filter来控制身份校验的流程。
package cn.sdu.sdupta.config;
import cn.sdu.sdupta.filter.JwtAuthenticationFilter;
import cn.sdu.sdupta.filter.OptionsRequestFilter;
import cn.sdu.sdupta.handler.UsernameLoginSuccessHandler;
import cn.sdu.sdupta.handler.JwtRefreshSuccessHandler;
import cn.sdu.sdupta.service.JwtUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlbasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
import java.util.Collections;
@EnableWebSecurity(debug = false)
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//授权
http.authorizeRequests()
.antMatchers("/user/login", "/user/admin/login", "/user/register", "/user/logout", "/test/anonymous/**").permitAll()
.anyRequest().authenticated()
.and().csrf().disable() //CSRF禁用,因为不使用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).disable() //禁用session
.formLogin().disable() //禁用form登录
.cors()
.and()
.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
//添加登录filter
.apply(new JsonLoginConfig<>())
.loginSuccessHandler(usernameLoginSuccessHandler())
.and()
//添加token的filter
.apply(new JwtLoginConfig<>())
.tokenValidSuccessHandler(jwtRefreshSuccessHandler())
.permissiveRequestUrls("/logout")
.and()
//使用默认的logoutFilter
.logout()
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()); //logout成功后返回200
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
PasswordEncoder getPw() {
return new BCryptPasswordEncoder();
}
@Bean
JwtAuthenticationFilter getJwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
protected UserDetailsService userDetailsService() {
return new JwtUserService();
}
@Bean("jwtUserService")
protected JwtUserService jwtUserService() {
return new JwtUserService();
}
@Bean
protected UsernameLoginSuccessHandler usernameLoginSuccessHandler() {
return new UsernameLoginSuccessHandler();
}
@Bean
protected JwtRefreshSuccessHandler jwtRefreshSuccessHandler() {
return new JwtRefreshSuccessHandler();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
configuration.addExposedHeader("Authorization");
UrlbasedCorsConfigurationSource source = new UrlbasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
jwtconfig: Java web token 的配置,使用JwtAuthenticationFilter作为过滤器实现具体功能。
package cn.sdu.sdupta.config; import cn.sdu.sdupta.filter.JwtAuthenticationFilter; import cn.sdu.sdupta.handler.HttpStatusLoginFailureHandler; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutFilter; @Configuration public class JwtLoginConfig, B extends HttpSecurityBuilder> extends AbstractHttpConfigurer { private final JwtAuthenticationFilter authFilter; public JwtLoginConfig() { this.authFilter = new JwtAuthenticationFilter(); } @Override public void configure(B http) { authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler()); //将filter放到logoutFilter之前 JwtAuthenticationFilter filter = postProcess(authFilter); http.addFilterBefore(filter, LogoutFilter.class); } //设置匿名用户可访问url public JwtLoginConfig permissiveRequestUrls(String... urls) { authFilter.setPermissiveUrl(urls); return this; } public JwtLoginConfig tokenValidSuccessHandler(AuthenticationSuccessHandler successHandler) { authFilter.setAuthenticationSuccessHandler(successHandler); return this; } }
JsonLoginConfig:使用UsernamePasswordAuthenticationFilter完成用户名密码的校验
package cn.sdu.sdupta.config; import cn.sdu.sdupta.filter.UsernamePasswordAuthenticationFilter; import cn.sdu.sdupta.handler.HttpStatusLoginFailureHandler; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; @Configuration public class JsonLoginConfig, B extends HttpSecurityBuilder> extends AbstractHttpConfigurer { private final UsernamePasswordAuthenticationFilter authFilter; public JsonLoginConfig() { this.authFilter = new UsernamePasswordAuthenticationFilter(); } @Override public void configure(B http) { //设置Filter使用的AuthenticationManager,这里取公共的即可 authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); //设置失败的Handler authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler()); //不将认证后的context放入session authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()); UsernamePasswordAuthenticationFilter filter = postProcess(authFilter); //指定Filter的位置 http.addFilterAfter(filter, LogoutFilter.class); } //设置成功的Handler,这个handler定义成Bean,所以从外面set进来 public JsonLoginConfig loginSuccessHandler(AuthenticationSuccessHandler authSuccessHandler) { authFilter.setAuthenticationSuccessHandler(authSuccessHandler); return this; } }
UsernamePasswordAuthenticationFilter:
package cn.sdu.sdupta.filter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
@WebFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/user/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username, password;
username = request.getParameter("username");
password = request.getParameter("password");
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
//封装到token中提交
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authRequest);
}
}
OptionsRequestFilter: 拦截options请求
package cn.sdu.sdupta.filter;
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 OptionsRequestFilter extends oncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getMethod().equals("OPTIONS")) {
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
response.setHeader("Access-Control-Allow-Headers", response.getHeader("Access-Control-Request-Headers"));
return;
}
filterChain.doFilter(request, response);
}
}
JwtAuthenticationFilter:对token进行校验,同时完成授权
package cn.sdu.sdupta.filter;
import cn.sdu.sdupta.domain.PtaUser;
import cn.sdu.sdupta.entity.ResultEntity;
import cn.sdu.sdupta.entity.StatusCode;
import cn.sdu.sdupta.util.JwtUtil;
import cn.sdu.sdupta.util.RedisUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
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;
import java.util.ArrayList;
import java.util.List;
public class JwtAuthenticationFilter extends oncePerRequestFilter {
private final RequestHeaderRequestMatcher requiresAuthenticationRequestMatcher;
private List permissiveRequestMatchers;
private AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
@Autowired
RedisUtil redisUtil;
public JwtAuthenticationFilter() {
//拦截header中带Authorization的请求
this.requiresAuthenticationRequestMatcher = new RequestHeaderRequestMatcher("Authorization");
}
protected String getJwtToken(HttpServletRequest request) {
return request.getHeader(JwtUtil.TOKEN_HEADER);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!requiresAuthentication(request)) {
chain.doFilter(request, response);
return;
}
String tokenHeader = getJwtToken(request);
if (tokenHeader == null || !tokenHeader.startsWith(JwtUtil.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
try {
if (!JwtUtil.isExpiration(tokenHeader.replace(JwtUtil.TOKEN_PREFIX, ""))) {
UsernamePasswordAuthenticationToken authentication = getAuthentication(tokenHeader);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (ExpiredJwtException e) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
response.getWriter().print(ResultEntity.error(StatusCode.USER_TOKEN_OVERDUE));
return;
} catch (JwtException e) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
response.getWriter().print(ResultEntity.error(StatusCode.USER_TOKEN_ERROR));
return;
} catch (Exception e) {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
response.getWriter().print(ResultEntity.error(StatusCode.COMMON_FAIL));
return;
}
chain.doFilter(request, response);
}
//获取用户信息
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "");
String account = JwtUtil.getUsername(token);
Integer userId = JwtUtil.getUserId(token);
if (account == null || userId == null) {
return null;
}
// 获得权限 添加到权限上去
String roles = JwtUtil.getUserRole(token);
PtaUser ptaUser = new PtaUser(account, "[PROTECTED]", AuthorityUtils.commaSeparatedStringToAuthorityList(roles));
ptaUser.setUserId(userId);
return new UsernamePasswordAuthenticationToken(ptaUser, null, AuthorityUtils.commaSeparatedStringToAuthorityList(roles));
}
protected boolean requiresAuthentication(HttpServletRequest request) {
return requiresAuthenticationRequestMatcher.matches(request);
}
protected AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
protected boolean permissiveRequest(HttpServletRequest request) {
if (permissiveRequestMatchers == null)
return false;
for (RequestMatcher permissiveMatcher : permissiveRequestMatchers) {
if (permissiveMatcher.matches(request))
return true;
}
return false;
}
public void setPermissiveUrl(String... urls) {
if (permissiveRequestMatchers == null)
permissiveRequestMatchers = new ArrayList<>();
for (String url : urls)
permissiveRequestMatchers.add(new AntPathRequestMatcher(url));
}
public void setAuthenticationSuccessHandler(
AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
public void setAuthenticationFailureHandler(
AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
protected AuthenticationSuccessHandler getSuccessHandler() {
return successHandler;
}
protected AuthenticationFailureHandler getFailureHandler() {
return failureHandler;
}
}
权限控制
不同控制方式可参考:
Spring Security 中的四种权限控制方式_江南一点雨的专栏-CSDN博客
我们将在下一次博客中详细介绍。



