[关闭]
@TedZhou 2020-11-04T09:56:20.000000Z 字数 20133 阅读 335

Spring security with thymeleaf

java spring security thymeleaf


记录spring boot项目使用spring security的核心配置和相关组件。要点:
1. 支持自定义页面登录
2. 支持AJAX登录/登出
3. 支持RBAC权限控制
4. 支持增加多种认证方式
5. 支持集群部署(会话共享redis存储)
6. 支持SessionId放在Header的X-Auth-Token里

项目依赖 pom.xml

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-security</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.thymeleaf.extras</groupId>
  15. <artifactId>thymeleaf-extras-springsecurity5</artifactId>
  16. </dependency>
  17. <dependency>
  18. <groupId>org.springframework.boot</groupId>
  19. <artifactId>spring-boot-starter-data-redis</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.session</groupId>
  23. <artifactId>spring-session-data-redis</artifactId>
  24. </dependency>

相关参考:关于redis 关于thymeleaf

Security配置类 SecurityConfig.java

  1. import java.util.Arrays;
  2. import java.util.List;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.context.annotation.Bean;
  5. import org.springframework.context.annotation.Configuration;
  6. import org.springframework.security.access.AccessDecisionManager;
  7. import org.springframework.security.access.AccessDecisionVoter;
  8. import org.springframework.security.access.vote.AuthenticatedVoter;
  9. import org.springframework.security.access.vote.UnanimousBased;
  10. import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
  11. import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
  12. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  13. import org.springframework.security.config.annotation.web.builders.WebSecurity;
  14. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  15. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  16. import org.springframework.security.web.access.expression.WebExpressionVoter;
  17. import org.springframework.security.web.authentication.AuthenticationFailureHandler;
  18. import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
  19. import org.springframework.session.web.http.HttpSessionIdResolver;
  20. @Configuration
  21. @EnableWebSecurity
  22. @EnableGlobalMethodSecurity(prePostEnabled = true)
  23. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  24. @Autowired
  25. private AuthProviderUsernamePassword authProviderUsernamePassword;
  26. @Autowired
  27. private AuthSuccessHandler authSuccessHandler;
  28. @Autowired
  29. private AuthFailureHandler authFailureHandler;
  30. @Autowired
  31. private ExitSuccessHandler exitSuccessHandler;
  32. @Bean
  33. protected AuthenticationFailureHandler authenticationFailureHandler() {
  34. authFailureHandler.setDefaultFailureUrl("/login?error");
  35. return authFailureHandler;
  36. }
  37. @Bean
  38. protected LogoutSuccessHandler logoutSuccessHandler() {
  39. exitSuccessHandler.setDefaultTargetUrl("/login?logout");
  40. return exitSuccessHandler;
  41. }
  42. private static String[] INGORE_URLS = {"/login", "/error",};
  43. @Override
  44. public void configure(WebSecurity webSecurity) {
  45. webSecurity.ignoring().antMatchers("/static/**");//忽略静态资源
  46. webSecurity.ignoring().antMatchers("/favicon.ico");
  47. }
  48. @Override
  49. protected void configure(HttpSecurity httpSecurity) throws Exception {
  50. httpSecurity
  51. .authorizeRequests()
  52. .antMatchers(INGORE_URLS).permitAll()
  53. .anyRequest().authenticated()
  54. .accessDecisionManager(accessDecisionManager())//如果不需要权限验证,去掉这句即可
  55. .and()
  56. .formLogin()
  57. .successHandler(authSuccessHandler)
  58. .failureHandler(authFailureHandler)
  59. .loginPage("/login")//.permitAll()
  60. .and()
  61. .logout()
  62. .logoutSuccessHandler(logoutSuccessHandler())//.permitAll()
  63. //.and().rememberMe()
  64. .and().csrf().disable();
  65. }
  66. @Override
  67. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  68. auth.authenticationProvider(authProviderUsernamePassword);
  69. //auth.authenticationProvider(authProvider2);可以增加多个认证方式,比如码验证等
  70. }
  71. @Bean
  72. protected AccessDecisionManager accessDecisionManager() {
  73. List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(
  74. new WebExpressionVoter(),
  75. authDecisionVoter(),//new RoleVoter(),
  76. new AuthenticatedVoter());
  77. return new UnanimousBased(decisionVoters);
  78. }
  79. @Bean
  80. protected AuthDecisionVoter authDecisionVoter() {
  81. return new AuthDecisionVoter();
  82. }
  83. @Bean
  84. public HttpSessionIdResolver httpSessionIdResolver() {
  85. return new HeaderCookieHttpSessionIdResolver();
  86. }
  87. }

登录认证类 AuthProviderUsernamePassword.java

AuthenticationProvider提供用户认证的处理方法。如果有多种认证方式,可以实现多个类一并添加到AuthenticationManagerBuilder里即可。

  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.security.authentication.AuthenticationProvider;
  3. import org.springframework.security.authentication.BadCredentialsException;
  4. import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.security.core.AuthenticationException;
  7. import org.springframework.stereotype.Component;
  8. @Component
  9. public class AuthProviderUsernamePassword implements AuthenticationProvider {
  10. @Autowired
  11. AuthUserService authUserService;
  12. @Override
  13. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  14. String username = authentication.getName();
  15. String password = authentication.getCredentials().toString();
  16. AuthUser userDetails = authUserService.loadUserByUsername(username);
  17. if(userDetails == null){
  18. throw new BadCredentialsException("账号或密码错误");
  19. }
  20. if (!authUserService.checkPassword(userDetails, password)) {
  21. throw new BadCredentialsException("账号或密码不正确");
  22. }
  23. //认证校验通过后,封装UsernamePasswordAuthenticationToken返回
  24. return new UsernamePasswordAuthenticationToken(userDetails, password, authUserService.fillUserAuthorities(userDetails));
  25. }
  26. @Override
  27. public boolean supports(Class<?> authentication) {
  28. return true;
  29. }
  30. }

登录成功处理 AuthSuccessHandler.java

配置于formLogin().successHandler(),可选。

  1. import java.io.IOException;
  2. import javax.servlet.ServletException;
  3. import javax.servlet.http.HttpServletRequest;
  4. import javax.servlet.http.HttpServletResponse;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
  7. import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
  8. import org.springframework.security.web.savedrequest.RequestCache;
  9. import org.springframework.security.web.savedrequest.SavedRequest;
  10. import org.springframework.stereotype.Component;
  11. @Component
  12. public class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
  13. private RequestCache requestCache = new HttpSessionRequestCache();
  14. @Override
  15. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
  16. Authentication authentication) throws ServletException, IOException {
  17. //登录成功处理,比如记录登录日志
  18. String ip = request.getRemoteAddr();
  19. String targetUrl = "";
  20. SavedRequest savedRequest = requestCache.getRequest(request, response);
  21. if (savedRequest != null) {
  22. targetUrl = savedRequest.getRedirectUrl();
  23. }
  24. AuthUser aUser = (AuthUser) authentication.getPrincipal();
  25. System.out.printf("User %s login, ip: %s, url: ", aUser.getUsername(), ip, targetUrl);
  26. if (WebUtils.isAjaxReq(request)) {//ajax登录
  27. HttpSession session = request.getSession();
  28. String sessionId = new Base64().encodeToString(session.getId().getBytes("UTF-8"));
  29. response.sendError(200, "success!SESSION="+sessionId);
  30. return;
  31. }
  32. super.onAuthenticationSuccess(request, response, authentication);
  33. }
  34. }

登录失败处理 AuthFailureHandler.java

配置于formLogin().failureHandler(),可选。

  1. import java.io.IOException;
  2. import javax.servlet.ServletException;
  3. import javax.servlet.http.HttpServletRequest;
  4. import javax.servlet.http.HttpServletResponse;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
  7. import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
  8. import org.springframework.security.web.savedrequest.RequestCache;
  9. import org.springframework.security.web.savedrequest.SavedRequest;
  10. import org.springframework.stereotype.Component;
  11. @Component
  12. public class AuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
  13. @Override
  14. public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
  15. AuthenticationException exception) throws IOException, ServletException {
  16. String uaSummary = WebUtils.getUserAgentSummary(request);
  17. String ip = request.getRemoteAddr();
  18. String username = request.getParameter("username");
  19. System.out.printf("User %s login failed, ip: %s, ua: %s", username, ip, uaSummary);
  20. super.saveException(request, exception);
  21. if (WebUtils.isAjaxReq(request)) {//ajax登录
  22. //为什么用sendError会导致302重定向到login页面?
  23. //--When you invoke sendError it will dispatch the request to /error (it the error handling code registered by Spring Boot. However, Spring Security will intercept /error and see that you are not authenticated and thus redirect you to a log in form.
  24. response.sendError(403, exception.getMessage());
  25. return;
  26. }
  27. response.sendRedirect("login?error");
  28. }
  29. }

登出成功处理 ExitSuccessHandler.java

配置于logout().logoutSuccessHandler(),可选。

  1. import java.io.IOException;
  2. import javax.servlet.ServletException;
  3. import javax.servlet.http.HttpServletRequest;
  4. import javax.servlet.http.HttpServletResponse;
  5. import org.springframework.security.core.Authentication;
  6. import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
  7. import org.springframework.stereotype.Component;
  8. @Component
  9. public class ExitSuccessHandler extends SimpleUrlLogoutSuccessHandler {
  10. @Override
  11. public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
  12. throws IOException, ServletException {
  13. if (WebUtils.isAjaxReq(request)) {//ajax登录
  14. response.sendError(200, "success");
  15. return;
  16. }
  17. super.onLogoutSuccess(request, response, authentication);
  18. }
  19. }

解析SessionId的类 HeaderCookieHttpSessionIdResolver.java

增加优先从Header里找X-Auth-Token作为SessionId,以适应不支持Cookie的情况。
这个类就是把CookieHttpSessionIdResolver和HeaderHttpSessionIdResolver柔和在一起而已。
对应配置@Bean httpSessionIdResolver。

  1. import java.util.List;
  2. import javax.servlet.http.HttpServletRequest;
  3. import javax.servlet.http.HttpServletResponse;
  4. import org.springframework.session.web.http.CookieHttpSessionIdResolver;
  5. import org.springframework.session.web.http.HeaderHttpSessionIdResolver;
  6. import org.springframework.session.web.http.HttpSessionIdResolver;
  7. public class HeaderCookieHttpSessionIdResolver implements HttpSessionIdResolver {
  8. protected HeaderHttpSessionIdResolver headerResolver = HeaderHttpSessionIdResolver.xAuthToken();
  9. protected CookieHttpSessionIdResolver cookieResolver = new CookieHttpSessionIdResolver();
  10. @Override
  11. public List<String> resolveSessionIds(HttpServletRequest request) {
  12. List<String> sessionIds = headerResolver.resolveSessionIds(request);
  13. if (sessionIds.isEmpty()) {
  14. sessionIds = cookieResolver.resolveSessionIds(request);
  15. }
  16. return sessionIds;
  17. }
  18. @Override
  19. public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
  20. headerResolver.setSessionId(request, response, sessionId);
  21. cookieResolver.setSessionId(request, response, sessionId);
  22. }
  23. @Override
  24. public void expireSession(HttpServletRequest request, HttpServletResponse response) {
  25. headerResolver.expireSession(request, response);
  26. cookieResolver.expireSession(request, response);
  27. }
  28. }

认证用户类 AuthUser.java

用户实体类,实现UserDetails接口。

  1. import java.io.Serializable;
  2. import java.util.Collection;
  3. import java.util.List;
  4. import javax.persistence.Id;
  5. import org.springframework.security.core.GrantedAuthority;
  6. import org.springframework.security.core.authority.AuthorityUtils;
  7. import org.springframework.security.core.userdetails.UserDetails;
  8. import org.springframework.util.StringUtils;
  9. import lombok.Data;
  10. @Data
  11. public class AuthUser implements UserDetails, Serializable {
  12. private static final long serialVersionUID = -1572872798317304041L;
  13. @Id
  14. private Long id;
  15. private String username;
  16. private String password;
  17. private Collection<? extends GrantedAuthority> authorities;
  18. public Collection<? extends GrantedAuthority> fillPerms(List<String> perms) {
  19. String authorityString = StringUtils.collectionToCommaDelimitedString(perms);
  20. authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authorityString);
  21. return authorities;
  22. }
  23. @Override
  24. public Collection<? extends GrantedAuthority> getAuthorities() {
  25. return authorities;
  26. }
  27. @Override
  28. public boolean isAccountNonExpired() {
  29. return true;
  30. }
  31. @Override
  32. public boolean isAccountNonLocked() {
  33. return true;
  34. }
  35. @Override
  36. public boolean isCredentialsNonExpired() {
  37. return true;
  38. }
  39. @Override
  40. public boolean isEnabled() {
  41. return true;
  42. }
  43. }

认证用户服务类 AuthUserService.java

提供根据用户名获取用户的方法loadUserByUsername();提供用户的权限fillUserAuthorities()。

  1. import java.util.ArrayList;
  2. import java.util.Collection;
  3. import java.util.List;
  4. import org.joda.time.LocalDateTime;
  5. import org.springframework.security.core.GrantedAuthority;
  6. import org.springframework.security.core.userdetails.UserDetailsService;
  7. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  8. import org.springframework.stereotype.Service;
  9. @Service
  10. public class AuthUserService implements UserDetailsService {
  11. @Override
  12. public AuthUser loadUserByUsername(String username) throws UsernameNotFoundException {
  13. //读取用户,一般是从数据库读取,这里随便new一个
  14. AuthUser user = new AuthUser();// userDao.findByUsername(username);
  15. user.setId(System.currentTimeMillis());
  16. user.setUsername(username);
  17. user.setPassword(username);
  18. return user;
  19. }
  20. public boolean checkPassword(AuthUser user, String pwd) {
  21. //判断用户密码,这里简单判断相等
  22. if (pwd != null && pwd.equals(user.getPassword())) {
  23. return true;
  24. }
  25. return false;
  26. }
  27. public Collection<? extends GrantedAuthority> fillUserAuthorities(AuthUser aUser) {
  28. //获取用户权限,一般从数据库读取,并缓存。这里随便拼凑
  29. List<String> perms = new ArrayList<>(); //permDao.findPermByUserId(aUser.getId());
  30. LocalDateTime now = LocalDateTime.now();
  31. perms.add("P"+now.getHourOfDay());
  32. perms.add("P"+now.getMinuteOfHour());
  33. perms.add("P"+now.getSecondOfMinute());
  34. return aUser.fillPerms(perms);
  35. }
  36. }

模拟用户示例:

  1. {
  2. "id": 1598515192490,
  3. "username": "test",
  4. "password": "test",
  5. "authorities": [{
  6. "authority": "P15"
  7. }, {
  8. "authority": "P59"
  9. }, {
  10. "authority": "P52"
  11. }
  12. ]
  13. }

认证入口 AuthControll.java

这里提供loginPage配置的路径"/login"。如果暂不想自定义登录界面,去掉loginPage配置即可。

  1. import org.springframework.security.core.annotation.AuthenticationPrincipal;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.ui.Model;
  4. import org.springframework.web.bind.annotation.PathVariable;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.ResponseBody;
  7. @Controller
  8. public class AuthController {
  9. @RequestMapping("/login")//登录入口
  10. String login(String username, Model model) {
  11. model.addAttribute("username", username);
  12. return "login";
  13. }
  14. @RequestMapping("/")//主页
  15. @ResponseBody
  16. Object home(@AuthenticationPrincipal AuthUser currentUser) {
  17. return currentUser;
  18. }
  19. @RequestMapping("/{path}")//测试用
  20. @ResponseBody
  21. Object url1(@PathVariable String path) {
  22. if (path.contains("0")) {//模拟错误
  23. path = String.valueOf(1/0);
  24. }
  25. return path;
  26. }
  27. }

权限验证类 AuthDecisionVoter.java

配置AccessDecisionManager用于自定义权限验证投票器。验证的前提是获取待访问资源(url)相关的权限(getPermissionsByUrl)。验证的方法是,看用户所拥有的权限是否能够匹配url的权限。

Spring security另一种常用的权限控制方式是配置@EnableGlobalMethodSecurity(prePostEnabled = true),在方法上使用@PreAuthorize("hasPermission('PXX')")。但用这种方法注解的url,不支持用在thymeleaf模板的sec:authorize-url中。

ps1.thymeleaf 提供了前端判断权限的扩展,参见 thymeleaf-extras-springsecurity & thymeleaf sec:标签的使用

  1. import java.util.ArrayList;
  2. import java.util.Collection;
  3. import java.util.List;
  4. import org.springframework.security.access.AccessDecisionVoter;
  5. import org.springframework.security.access.ConfigAttribute;
  6. import org.springframework.security.access.SecurityConfig;
  7. import org.springframework.security.core.Authentication;
  8. import org.springframework.security.core.GrantedAuthority;
  9. import org.springframework.security.web.FilterInvocation;
  10. import org.springframework.util.StringUtils;
  11. public class RbacDecisionVoter implements AccessDecisionVoter<Object> {
  12. static final String permitAll = "permitAll";
  13. @Override
  14. public boolean supports(ConfigAttribute attribute) {
  15. return true;
  16. }
  17. @Override
  18. public boolean supports(Class<?> clazz) {
  19. return true;
  20. }
  21. @Override
  22. public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
  23. if (authentication == null) {
  24. return ACCESS_DENIED;
  25. }
  26. if (attributes != null) {
  27. for (ConfigAttribute attribute : attributes) {
  28. if (permitAll.equals(attribute.toString())) {// skip permitAll
  29. return ACCESS_ABSTAIN;
  30. }
  31. }
  32. }
  33. String requestUrl = ((FilterInvocation) object).getRequestUrl();// 当前请求的URL
  34. Collection<ConfigAttribute> urlPerms = getPermissionsByUrl(requestUrl);// 能访问URL的权限
  35. if (urlPerms == null || urlPerms.isEmpty()) {
  36. return ACCESS_ABSTAIN;
  37. }
  38. int result = ACCESS_ABSTAIN;
  39. Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities(); // 当前用户的权限
  40. for (ConfigAttribute attribute : urlPerms) {
  41. String urlPerm = attribute.getAttribute();
  42. if (StringUtils.isEmpty(urlPerm)) {
  43. continue;
  44. }
  45. result = ACCESS_DENIED;
  46. // Attempt to find a matching granted authority
  47. for (GrantedAuthority authority : userAuthorities) {
  48. if (urlPerm.equals(authority.getAuthority())) {
  49. return ACCESS_GRANTED;
  50. }
  51. }
  52. }
  53. return result;
  54. }
  55. Collection<ConfigAttribute> getPermissionsByUrl(String url) {
  56. // 获取url的访问权限,一般从数据库读取,并缓存。这里随便拼凑
  57. if ("/".equals(url)) {
  58. return null;//根路径不限权
  59. }
  60. String n1 = url.substring(url.length()-1);
  61. String n2 = url.substring(url.length()-2);
  62. return SecurityConfig.createList("P"+n1, "P"+n2);
  63. }
  64. }

自定义登录界面 login.html

  1. <!DOCTYPE html>
  2. <html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  3. <head>
  4. <title>登录</title>
  5. <meta charset="utf-8"/>
  6. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"/>
  7. <link rel="stylesheet" href="//cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css"/>
  8. <style type="text/css">
  9. body{padding-top:40px; padding-bottom:40px; background-color:#eee;}
  10. .form-signin{max-width:330px; padding:15px; margin:0 auto;}
  11. </style>
  12. </head>
  13. <body>
  14. <div id="root" class="container">
  15. <form class="form-signin" method="post" th:action="@{/login}">
  16. <h2 class="form-signin-heading">请登录</h2>
  17. <div th:if="${param.logout}" class="alert alert-success" role="alert"><span>您已退出登录</span></div>
  18. <div th:if="${param.error}" class="alert alert-danger" role="alert"><span th:utext="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}">密码错误</span></div>
  19. <p>
  20. <label for="username" class="sr-only">用户账号:</label>
  21. <input type="text" id="username" name="username" class="form-control" placeholder="请输入账号" required autofocus>
  22. </p>
  23. <p>
  24. <label for="password" class="sr-only">用户密码:</label>
  25. <input type="password" name="password" class="form-control" placeholder="请输入密码" required>
  26. </p>
  27. <button class="btn btn-lg btn-primary btn-block" type="submit">确定</button>
  28. </form>
  29. </div>
  30. </body>
  31. </html>

自定义错误信息 CustomErrorAttributes.java

403-没有权限、404-找不到页面等所有错误和异常,都会被SpringBoot默认的BasicErrorController处理。如果有需要,可定制ErrorAttributes。

  1. import java.util.Map;
  2. import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.web.context.request.WebRequest;
  5. @Component
  6. public class CustomErrorAttributes extends DefaultErrorAttributes {
  7. @Override
  8. public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
  9. Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
  10. errorAttributes.put("code", errorAttributes.getOrDefault("status", 0));//自定义code属性
  11. Throwable error = super.getError(webRequest);
  12. if (error != null && error.getMessage() != null) {
  13. String message = (String)errorAttributes.getOrDefault("message", "");
  14. if (!message.equals(error.getMessage())) {
  15. errorAttributes.put("message", message+" "+error.getMessage());//增强message属性
  16. }
  17. }
  18. return errorAttributes;
  19. }
  20. }

非浏览器访问(produces="text/html")出错时,返回json数据,示例:

  1. {
  2. "timestamp": "2020-08-27T09:05:11.178+0000",
  3. "status": 500,
  4. "error": "Internal Server Error",
  5. "message": "/ by zero",
  6. "path": "/demo/015",
  7. "code": 500
  8. }

浏览器访问(produces="text/html")出错时,返回html页面。

自定义错误页面 error/4xx.html

SpringBoot默认的Whitelabel Error Page需要定制,只要把错误页面模板放在error路径下即可。模板中可使用上述ErrorAttributes中的字段。

  1. <!DOCTYPE html>
  2. <html lang="zh" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  3. <head>
  4. <meta charset="utf-8"/>
  5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"/>
  6. <link rel="stylesheet" href="//cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css"/>
  7. </head>
  8. <body>
  9. <div id="root" class="container">
  10. <div class="main">
  11. <br/><h2 class="text-center"><span th:text="${status}">404</span>-<span th:text="${error}">Not Found</span></h2><br/>
  12. <p class="text-center" th:if="${message}"><span th:text="${message}"></span></p>
  13. <p class="text-center" th:if="${exception}"><span th:text="${exception}"></span></p>
  14. <p class="text-center"><a class="btn btn-primary" th:href="@{'/'}">Home</a></p>
  15. </div>
  16. </div>
  17. </body>
  18. </html>

自定义错误页面 error/5xx.html

类似5xx.html,略。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注