单元测试 /** * 用户查询单测 * */ @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
