单元测试 /** * 用户查询单测 * */ @Test public void whenQuerySuccess() throws Exception { String result = mockMvc.perform( get("/user") .param("username", "jojo") .param("age", "18") .param("ageTo", "60") .param("xxx", "yyy") // .param("size", "15") // .param("page", "3") // .param("sort", "age,desc") .contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(jsonPath("$.length()").value(3)) // 结果期望 .andReturn().getResponse().getContentAsString(); System.out.println(result); }
@GetMapping @JsonView(User.UserSimpleView.class) @ApiOperation(value = "用户查询服务") public List<User> query(UserQueryCondition condition, @PageableDefault(page = 2, size = 17, sort = "username,asc") Pageable pageable) { System.out.println(ReflectionToStringBuilder.toString(condition, ToStringStyle.MULTI_LINE_STYLE)); System.out.println(pageable.getPageSize()); System.out.println(pageable.getPageNumber()); System.out.println(pageable.getSort()); List<User> users = new ArrayList<>(); users.add(new User()); users.add(new User()); users.add(new User()); return users; }
/** * 用户详情 * * @param id 用户id * :\d+ 只能接受数字 */ @GetMapping("/{id:\\d+}") @JsonView(User.UserDetailView.class) public User getInfo(@ApiParam("用户id") @PathVariable String id) { // throw new RuntimeException("user not exist"); System.out.println("进入getInfo服务"); User user = new User(); user.setUsername("tom"); return user; }
@Test
public void whenGetInfoFail() throws Exception {
mockMvc.perform(get("/user/a")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().is4xxClientError());
}
@GetMapping("/{id:\\d+}") 与 @RequestMapping(value = "/{id:\\d+}",method = RequestMethod.GET) 等价
@JsonView使用步骤
- 使用接口声明多个视图
// 用户简单视图 public interface UserSimpleView {}; // 用户详细视图 public interface UserDetailView extends UserSimpleView {};
- 在值对象的get方法上指定视图
说明:在UserDetailView显示密码 @JsonView(UserSimpleView.class) public String getUsername() { return username; } @JsonView(UserDetailView.class) public String getPassword() { return password; }
- 在controller方法上指定视图
@GetMapping @JsonView(User.UserSimpleView.class)
或者
@JsonView(User.UserDetailView.class)
常用验证注解
@NotBlank(message = "密码不能为空")
public User create(@Valid @RequestBody User user, BindingResult errors) {
if (errors.hasErrors()) {
errors.getAllErrors().forEach(error -> System.out.println(error.getDefaultMessage()));
}
spring boot错误处理方式
org.springframework.boot.autoconfigure.web.BasicErrorControlle
根据请求头中,选择返回形式
例如
@RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView("error", model) : modelAndView); } @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); }
自定义404和500页面
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>404</title> </head> <body> 您所访问的页面不存在 </body> </html>
过滤器
/** * */ package com.imooc.web.filter; import javax.servlet.*; import java.io.IOException; import java.util.Date; /** * @author zhailiang */ //@Component public class TimeFilter implements Filter { /* (non-Javadoc) * @see javax.servlet.Filter#destroy() */ @Override public void destroy() { System.out.println("time filter destroy"); } /* (non-Javadoc) * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("time filter start"); long start = new Date().getTime(); chain.doFilter(request, response); System.out.println("time filter 耗时:" + (new Date().getTime() - start)); System.out.println("time filter finish"); } /* (non-Javadoc) * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) */ @Override public void init(FilterConfig arg0) throws ServletException { System.out.println("time filter init"); } }
控制台
引入第三方过滤器
/** * */ package com.imooc.web.config; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import com.imooc.web.filter.TimeFilter; import com.imooc.web.interceptor.TimeInterceptor; /** * @author zhailiang * */ @Configuration public class WebConfig extends WebMvcConfigurerAdapter { @SuppressWarnings("unused") @Autowired private TimeInterceptor timeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // registry.addInterceptor(timeInterceptor); } // @Bean public FilterRegistrationBean timeFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); TimeFilter timeFilter = new TimeFilter(); registrationBean.setFilter(timeFilter); List<String> urls = new ArrayList<>(); urls.add("/*"); registrationBean.setUrlPatterns(urls); return registrationBean; } }
拦截器
/** * */ package com.imooc.web.interceptor; import java.util.Date; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** * @author zhailiang * */ @Component public class TimeInterceptor implements HandlerInterceptor { /* (non-Javadoc) * @see org.springframework.web.servlet.HandlerInterceptor#preHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object) */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle"); System.out.println(((HandlerMethod)handler).getBean().getClass().getName()); System.out.println(((HandlerMethod)handler).getMethod().getName()); request.setAttribute("startTime", new Date().getTime()); return true; } /* (non-Javadoc) * @see org.springframework.web.servlet.HandlerInterceptor#postHandle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, org.springframework.web.servlet.ModelAndView) */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle"); Long start = (Long) request.getAttribute("startTime"); System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start)); } /* (non-Javadoc) * @see org.springframework.web.servlet.HandlerInterceptor#afterCompletion(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.lang.Object, java.lang.Exception) */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion"); Long start = (Long) request.getAttribute("startTime"); System.out.println("time interceptor 耗时:"+ (new Date().getTime() - start)); System.out.println("ex is "+ex); } }
@Configuration public class WebConfig extends WebMvcConfigurerAdapter { @SuppressWarnings("unused") @Autowired private TimeInterceptor timeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(timeInterceptor); }
总结:拦截器会拦截所有控制层接口,包括spring的
------------------------------------------------------------------
org.springframework.web.servlet.DispatcherServlet
org.springframework.web.servlet.DispatcherServlet#doService
if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. 真正调用处理器 (方法处理器在下面 handle()) mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; }
拦截器缺陷:无法获取请求参数等信息,可以使用spring切片解决。
切片
/** * */ package com.imooc.web.aspect; import java.util.Date; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; /** * @author zhailiang * */ @Aspect @Component public class TimeAspect { @Around("execution(* com.imooc.web.controller.UserController.*(..))") public Object handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable { System.out.println("time aspect start"); Object[] args = pjp.getArgs(); for (Object arg : args) { System.out.println("arg is "+arg); } long start = new Date().getTime(); Object object = pjp.proceed(); // 获取返回参数,但是拿不到request信息 System.out.println("time aspect 耗时:"+ (new Date().getTime() - start)); System.out.println("time aspect end"); return object; } }
总图
官网:
webster.com/dictionary/wirework
模拟接口,给前端访问,前后端并行开发。
/** * */ package com.imooc.wiremock; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.springframework.core.io.ClassPathResource; import java.io.IOException; import static com.github.tomakehurst.wiremock.client.WireMock.*; /** * */ public class MockServer { public static void main(String[] args) throws IOException { configureFor(8062); removeAllMappings(); mock("/order/1", "01"); mock("/order/2", "02"); } /** * @param url 文件url * @param file 文件名称 */ private static void mock(String url, String file) throws IOException { ClassPathResource resource = new ClassPathResource("mock/response/" + file + ".txt"); String content = StringUtils.join(FileUtils.readLines(resource.getFile(), "UTF-8").toArray(), "\n"); stubFor(get(urlPathEqualTo(url)).willReturn(aResponse().withBody(content).withStatus(200))); } }
讲解上图过程:
地址栏输入一个请求:
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke
if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null);
异常捕获
org.springframework.security.web.access.ExceptionTranslationFilter#doFilter
catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); } if (ase != null) { handleSpringSecurityException(request, response, chain, ase); }
异常处理会重定向到一个登陆页面
点击登录,到
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
账户密码校验ok,又会到org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke
这次对地址栏请求接口进行处理
public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
doFilter()则进入后台控制层接口/user接口,并返回数据。
org.springframework.security.access.intercept.AbstractSecurityInterceptor#beforeInvocation
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#obtainUsername
org.springframework.security.web.util.ThrowableAnalyzer#determineCauseChain
根据用户名查询
org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername
public interface UserDetailsService { // ~ Methods // ======================================================================================================== /** * Locates the user based on the username. In the actual implementation, the search * may possibly be case sensitive, or case insensitive depending on how the * implementation instance is configured. In this case, the <code>UserDetails</code> * object that comes back may have a username that is of a different case than what * was actually requested.. * * @param username the username identifying the user whose data is required. * * @return a fully populated user record (never <code>null</code>) * * @throws UsernameNotFoundException if the user could not be found or the user has no * GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
用户逻辑校验
public SocialUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); } public SocialUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); }
new SocialUser(userId, password, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
密码加密
package org.springframework.security.crypto.password; public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); }
实际
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private PasswordEncoder passwordEncoder;
String password = passwordEncoder.encode("123456"); logger.info("数据库密码是:"+password); // 密文 每次不一样 会自动加盐 增加安全性
记住我
源码追踪
正向记住:
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }
org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices#onLoginSuccess
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); logger.debug("Creating new persistent login for user " + username); PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), new Date()); try { tokenRepository.createNewToken(persistentToken); // 创建新token addCookie(persistentToken, request, response); // 写入浏览器 } catch (Exception e) { logger.error("Failed to save persistent token ", e); } }
校验登陆
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (SecurityContextHolder.getContext().getAuthentication() == null) { Authentication rememberMeAuth = rememberMeServices.autoLogin(request, // 调用自动登陆 response); if (rememberMeAuth != null) { // Attempt authenticaton via AuthenticationManager try { rememberMeAuth = authenticationManager.authenticate(rememberMeAuth); // Store to SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); onSuccessfulAuthentication(request, response, rememberMeAuth); if (logger.isDebugEnabled()) { logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } // Fire event if (this.eventPublisher != null) { eventPublisher .publishEvent(new InteractiveAuthenticationSuccessEvent( SecurityContextHolder.getContext() .getAuthentication(), this.getClass())); } if (successHandler != null) { successHandler.onAuthenticationSuccess(request, response, rememberMeAuth); return; } } catch (AuthenticationException authenticationException) { if (logger.isDebugEnabled()) { logger.debug( "SecurityContextHolder not populated with remember-me token, as " + "AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", authenticationException); } rememberMeServices.loginFail(request, response); onUnsuccessfulAuthentication(request, response, authenticationException); } } chain.doFilter(request, response); } else { if (logger.isDebugEnabled()) { logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } chain.doFilter(request, response); } }
org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices#processAutoLoginCookie
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { if (cookieTokens.length != 2) { throw new InvalidCookieException("Cookie token did not contain " + 2 + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'"); } final String presentedSeries = cookieTokens[0]; final String presentedToken = cookieTokens[1]; PersistentRememberMeToken token = tokenRepository .getTokenForSeries(presentedSeries); // 从数据库拿token if (token == null) { // No series match, so we can't authenticate using this cookie throw new RememberMeAuthenticationException( "No persistent token found for series id: " + presentedSeries); }
if (token == null) {
// No series match, so we can't authenticate using this cookie
throw new RememberMeAuthenticationException(
"No persistent token found for series id: " + presentedSeries);
}
// We have a match for this user/series combination
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw
// an exception to warn them.
tokenRepository.removeUserTokens(token.getUsername());
throw new CookieTheftException(
messages.getMessage(
"PersistentTokenBasedRememberMeServices.cookieStolen",
"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
}
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}
// Token also matches, so login is valid. Update the token value, keeping the
// *same* series number.
if (logger.isDebugEnabled()) {
logger.debug("Refreshing persistent login token for user '"
+ token.getUsername() + "', series '" + token.getSeries() + "'");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(
token.getUsername(), token.getSeries(), generateTokenData(), new Date());
try {
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception e) {
logger.error("Failed to update token: ", e);
throw new RememberMeAuthenticationException(
"Autologin failed due to data access problem");
}
return getUserDetailsService().loadUserByUsername(token.getUsername()); // 获取用户信息
}
登陆了
来源:oschina
链接:https://my.oschina.net/u/3915790/blog/3213098