Spring Boot Security
如图,是一种通用的用户权限模型。一般情况下会有 5 张表,分别是:用户表,角色表,权限表,用户角色关系表,角色权限对应表。
一般,资源分配时是基于角色的(即,资源访问权限赋给角色,用户通过角色进而拥有权限);而访问资源的时候是基于资源权限去进行授权判断的。
Spring Security 和 Apache Shiro 是两个应用比较多的权限管理框架。Spring Security 依赖 Spring,其功能强大,相对于 Shiro 而言学习难度稍大一些。
Spring 的强大是不言而喻的,可扩展性也很强,强大到用 Spring 家族的产品只要按照其推荐的做法来就非常非常简单,否则,自己去整合过程可能会很痛苦。
目前,我们项目是基于 Spring Boot 的,而且 Spring Boot 的权限管理也是推荐使用 Spring Security 的,所以再难也是要学习的。
Spring Security 简介
Spring Security 致力于为 Java 应用提供认证和授权管理。它是一个强大的,高度自定义的认证和访问控制框架。
具体介绍参见 https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/
这句话包括两个关键词:Authentication(认证)和 Authorization(授权,也叫访问控制)
认证是验证用户身份的合法性,而授权是控制你可以做什么。
简单地来说,认证就是你是谁,授权就是你可以做什么。
在开始集成之前,我们先简单了解几个接口:
AuthenticationProvider
AuthenticationProvider 接口是用于认证的,可以通过实现这个接口来定制我们自己的认证逻辑,它的实现类有很多,默认的是 JaasAuthenticationProvider
它的全称是 Java Authentication and Authorization Service (JAAS)
AccessDecisionManager
AccessDecisionManager 是用于访问控制的,它决定用户是否可以访问某个资源,实现这个接口可以定制我们自己的授权逻辑。
AccessDecisionVoter
AccessDecisionVoter 是投票器,在授权的时通过投票的方式来决定用户是否可以访问,这里涉及到投票规则。
UserDetailsService
UserDetailsService 是用于加载特定用户信息的,它只有一个接口通过指定的用户名去查询用户。
UserDetails
UserDetails 代表用户信息,即主体,相当于 Shiro 中的 Subject。User 是它的一个实现。
Spring Boot 集成 Spring Security
按照官方文档的说法,为了定义我们自己的认证管理,我们可以添加 UserDetailsService, AuthenticationProvider, or AuthenticationManager 这种类型的 Bean。
实现的方式有多种,这里我选择最简单的一种(因为本身我们这里的认证授权也比较简单)
通过定义自己的 UserDetailsService 从数据库查询用户信息,至于认证的话就用默认的。
Maven 依赖
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 6 <groupId>com.cjs.example</groupId> 7 <artifactId>cjs-springsecurity-example</artifactId> 8 <version>0.0.1-SNAPSHOT</version> 9 <packaging>jar</packaging> 10 11 <name>cjs-springsecurity-example</name> 12 <description></description> 13 14 <parent> 15 <groupId>org.springframework.boot</groupId> 16 <artifactId>spring-boot-starter-parent</artifactId> 17 <version>2.0.2.RELEASE</version> 18 <relativePath/> <!-- lookup parent from repository --> 19 </parent> 20 21 <properties> 22 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 23 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 24 <java.version>1.8</java.version> 25 </properties> 26 27 <dependencies> 28 <dependency> 29 <groupId>org.springframework.boot</groupId> 30 <artifactId>spring-boot-starter-cache</artifactId> 31 </dependency> 32 <dependency> 33 <groupId>org.springframework.boot</groupId> 34 <artifactId>spring-boot-starter-data-redis</artifactId> 35 </dependency> 36 <dependency> 37 <groupId>org.springframework.boot</groupId> 38 <artifactId>spring-boot-starter-security</artifactId> 39 </dependency> 40 <dependency> 41 <groupId>org.springframework.boot</groupId> 42 <artifactId>spring-boot-starter-thymeleaf</artifactId> 43 </dependency> 44 <dependency> 45 <groupId>org.springframework.boot</groupId> 46 <artifactId>spring-boot-starter-web</artifactId> 47 </dependency> 48 <dependency> 49 <groupId>org.thymeleaf.extras</groupId> 50 <artifactId>thymeleaf-extras-springsecurity4</artifactId> 51 <version>3.0.2.RELEASE</version> 52 </dependency> 53 54 55 <dependency> 56 <groupId>org.projectlombok</groupId> 57 <artifactId>lombok</artifactId> 58 <optional>true</optional> 59 </dependency> 60 <dependency> 61 <groupId>org.springframework.boot</groupId> 62 <artifactId>spring-boot-starter-test</artifactId> 63 <scope>test</scope> 64 </dependency> 65 <dependency> 66 <groupId>org.springframework.security</groupId> 67 <artifactId>spring-security-test</artifactId> 68 <scope>test</scope> 69 </dependency> 70 </dependencies> 71 72 <build> 73 <plugins> 74 <plugin> 75 <groupId>org.springframework.boot</groupId> 76 <artifactId>spring-boot-maven-plugin</artifactId> 77 </plugin> 78 </plugins> 79 </build> 80 81 </project>
Security 配置
1 package com.cjs.example.config; 2 3 import com.cjs.example.support.MyUserDetailsService; 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 9 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 import org.springframework.security.crypto.password.PasswordEncoder; 14 15 @Configuration 16 @EnableWebSecurity 17 @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证 18 public class SecurityConfig extends WebSecurityConfigurerAdapter { 19 20 @Autowired 21 private MyUserDetailsService myUserDetailsService; 22 23 24 @Override 25 protected void configure(HttpSecurity http) throws Exception { 26 // 允许所有用户访问 "/" 和 "/index.html" 27 http.authorizeRequests() 28 .antMatchers("/", "/index.html").permitAll() 29 .anyRequest().authenticated() // 其他地址的访问均需验证权限 30 .and() 31 .formLogin() 32 .loginPage("/login.html") // 登录页 33 .failureUrl("/login-error.html").permitAll() 34 .and() 35 .logout() 36 .logoutSuccessUrl("/index.html"); 37 } 38 39 @Override 40 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 41 auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); 42 } 43 44 @Bean 45 public PasswordEncoder passwordEncoder() { 46 return new BCryptPasswordEncoder(); 47 } 48 49 }
MyUserDetailsService
1 package com.cjs.example.support; 2 3 import com.cjs.example.entity.SysPermission; 4 import com.cjs.example.entity.SysRole; 5 import com.cjs.example.entity.SysUser; 6 import com.cjs.example.service.UserService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 import org.springframework.security.core.userdetails.User; 10 import org.springframework.security.core.userdetails.UserDetails; 11 import org.springframework.security.core.userdetails.UserDetailsService; 12 import org.springframework.security.core.userdetails.UsernameNotFoundException; 13 import org.springframework.stereotype.Service; 14 15 import java.util.ArrayList; 16 import java.util.List; 17 18 @Service 19 public class MyUserDetailsService implements UserDetailsService { 20 21 @Autowired 22 private UserService userService; 23 24 /** 25 * 授权的时候是对角色授权,而认证的时候应该基于资源,而不是角色,因为资源是不变的,而用户的角色是会变的 26 */ 27 28 @Override 29 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 30 SysUser sysUser = userService.getUserByName(username); 31 if (null == sysUser) { 32 throw new UsernameNotFoundException(username); 33 } 34 List<SimpleGrantedAuthority> authorities = new ArrayList<>(); 35 for (SysRole role : sysUser.getRoleList()) { 36 for (SysPermission permission : role.getPermissionList()) { 37 authorities.add(new SimpleGrantedAuthority(permission.getCode())); 38 } 39 } 40 41 return new User(sysUser.getUsername(), sysUser.getPassword(), authorities); 42 } 43 }
权限分配
1 package com.cjs.example.service.impl; 2 3 import com.cjs.example.dao.UserDao; 4 import com.cjs.example.entity.SysUser; 5 import com.cjs.example.service.UserService; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.cache.annotation.Cacheable; 8 import org.springframework.stereotype.Service; 9 10 @Service 11 public class UserServiceImpl implements UserService { 12 13 @Autowired 14 private UserDao userDao; 15 16 @Cacheable(cacheNames = "authority", key = "#username") 17 @Override 18 public SysUser getUserByName(String username) { 19 return userDao.selectByName(username); 20 } 21 }
1 package com.cjs.example.dao; 2 3 import com.cjs.example.entity.SysPermission; 4 import com.cjs.example.entity.SysRole; 5 import com.cjs.example.entity.SysUser; 6 import lombok.extern.slf4j.Slf4j; 7 import org.springframework.stereotype.Repository; 8 9 import java.util.Arrays; 10 11 @Slf4j 12 @Repository 13 public class UserDao { 14 15 private SysRole admin = new SysRole("ADMIN", "管理员"); 16 private SysRole developer = new SysRole("DEVELOPER", "开发者"); 17 18 { 19 SysPermission p1 = new SysPermission(); 20 p1.setCode("UserIndex"); 21 p1.setName("个人中心"); 22 p1.setUrl("/user/index.html"); 23 24 SysPermission p2 = new SysPermission(); 25 p2.setCode("BookList"); 26 p2.setName("图书列表"); 27 p2.setUrl("/book/list"); 28 29 SysPermission p3 = new SysPermission(); 30 p3.setCode("BookAdd"); 31 p3.setName("添加图书"); 32 p3.setUrl("/book/add"); 33 34 SysPermission p4 = new SysPermission(); 35 p4.setCode("BookDetail"); 36 p4.setName("查看图书"); 37 p4.setUrl("/book/detail"); 38 39 admin.setPermissionList(Arrays.asList(p1, p2, p3, p4)); 40 developer.setPermissionList(Arrays.asList(p1, p2)); 41 42 } 43 44 public SysUser selectByName(String username) { 45 log.info("从数据库中查询用户"); 46 if ("zhangsan".equals(username)) { 47 SysUser sysUser = new SysUser("zhangsan", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm"); 48 sysUser.setRoleList(Arrays.asList(admin, developer)); 49 return sysUser; 50 }else if ("lisi".equals(username)) { 51 SysUser sysUser = new SysUser("lisi", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm"); 52 sysUser.setRoleList(Arrays.asList(developer)); 53 return sysUser; 54 } 55 return null; 56 } 57 58 }
示例
这里我设计的例子是用户登录成功以后跳到个人中心,然后用户可以可以进入图书列表查看。
用户 zhangsan 可以查看所有的,而 lisi 只能查看图书列表,不能添加不能查看详情。
页面设计
LoginController.java
1 package com.cjs.example.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.ui.Model; 5 import org.springframework.web.bind.annotation.RequestMapping; 6 7 @Controller 8 public class LoginController { 9 10 // Login form 11 @RequestMapping("/login.html") 12 public String login() { 13 return "login.html"; 14 } 15 16 // Login form with error 17 @RequestMapping("/login-error.html") 18 public String loginError(Model model) { 19 model.addAttribute("loginError", true); 20 return "login.html"; 21 } 22 23 }
BookController.java
1 package com.cjs.example.controller; 2 3 import org.springframework.security.access.prepost.PreAuthorize; 4 import org.springframework.stereotype.Controller; 5 import org.springframework.web.bind.annotation.GetMapping; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 8 @Controller 9 @RequestMapping("/book") 10 public class BookController { 11 12 @PreAuthorize("hasAuthority('BookList')") 13 @GetMapping("/list.html") 14 public String list() { 15 return "book/list"; 16 } 17 18 @PreAuthorize("hasAuthority('BookAdd')") 19 @GetMapping("/add.html") 20 public String add() { 21 return "book/add"; 22 } 23 24 @PreAuthorize("hasAuthority('BookDetail')") 25 @GetMapping("/detail.html") 26 public String detail() { 27 return "book/detail"; 28 } 29 }
UserController.java
1 package com.cjs.example.controller; 2 3 import com.cjs.example.entity.SysUser; 4 import com.cjs.example.service.UserService; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.security.access.prepost.PreAuthorize; 7 import org.springframework.stereotype.Controller; 8 import org.springframework.web.bind.annotation.GetMapping; 9 import org.springframework.web.bind.annotation.RequestMapping; 10 import org.springframework.web.bind.annotation.ResponseBody; 11 12 @Controller 13 @RequestMapping("/user") 14 public class UserController { 15 16 @Autowired 17 private UserService userService; 18 19 /** 20 * 个人中心 21 */ 22 @PreAuthorize("hasAuthority('UserIndex')") 23 @GetMapping("/index") 24 public String index() { 25 return "user/index"; 26 } 27 28 @RequestMapping("/hi") 29 @ResponseBody 30 public String hi() { 31 SysUser sysUser = userService.getUserByName("zhangsan"); 32 return sysUser.toString(); 33 } 34 35 }
index.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>首页</title> 6 </head> 7 <body> 8 <h2>这里是首页</h2> 9 </body> 10 </html>
login.html
1 <!DOCTYPE html> 2 <html lang="zh" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Login page</title> 6 </head> 7 <body> 8 <h1>Login page</h1> 9 <p th:if="${loginError}" class="error">用户名或密码错误</p> 10 <form th:action="@{/login.html}" method="post"> 11 <label for="username">Username</label>: 12 <input type="text" id="username" name="username" autofocus="autofocus" /> <br /> 13 <label for="password">Password</label>: 14 <input type="password" id="password" name="password" /> <br /> 15 <input type="submit" value="Login" /> 16 </form> 17 </body> 18 </html>
/user/index.html
1 <!DOCTYPE html> 2 <html lang="zh" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>个人中心</title> 6 </head> 7 <body> 8 <h2>个人中心</h2> 9 <div th:insert="~{fragments/header::logout}"></div> 10 <a href="/book/list.html">图书列表</a> 11 </body> 12 </html>
/book/list.html
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> <meta charset="UTF-8"> <title>图书列表</title> </head> <body> <div th:insert="~{fragments/header::logout}"></div> <h2>图书列表</h2> <div sec:authorize="hasAuthority('BookAdd')"> <button onclick="">添加</button> </div> <table border="1" cellspacing="0" style="width: 20%"> <thead> <tr> <th>名称</th> <th>出版社</th> <th>价格</th> <th>操作</th> </tr> </thead> <tbody> <tr> <td>Java 从入门到放弃</td> <td>机械工业出版社</td> <td>39</td> <td><span sec:authorize="hasAuthority('BookDetail')"><a href="/book/detail.html">查看</a></span></td> </tr> <tr> <td>MySQ 从删库到跑路</td> <td>清华大学出版社</td> <td>59</td> <td><span sec:authorize="hasAuthority('BookDetail')"><a href="/book/detail.html">查看</a></span></td> </tr> </tbody> </table> </body> </html>
header.html
1 <!DOCTYPE html> 2 <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> 3 <body> 4 <div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> 5 Logged in user: <span sec:authentication="name"></span> | 6 Roles: <span sec:authentication="principal.authorities"></span> 7 <div> 8 <form action="#" th:action="@{/logout}" method="post"> 9 <input type="submit" value="退出" /> 10 </form> 11 </div> 12 </div> 13 </body> 14 </html>
错误处理
ErrorController.java
1 package com.cjs.example.controller; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.http.HttpStatus; 5 import org.springframework.ui.Model; 6 import org.springframework.web.bind.annotation.ControllerAdvice; 7 import org.springframework.web.bind.annotation.ExceptionHandler; 8 import org.springframework.web.bind.annotation.ResponseStatus; 9 10 @Slf4j 11 @ControllerAdvice 12 public class ErrorController { 13 14 @ExceptionHandler(Throwable.class) 15 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 16 public String exception(final Throwable throwable, final Model model) { 17 log.error("Exception during execution of SpringSecurity application", throwable); 18 String errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error"); 19 model.addAttribute("errorMessage", errorMessage); 20 return "error"; 21 } 22 23 }
error.html
1 <!DOCTYPE html> 2 <html xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <title>Error page</title> 5 <meta charset="utf-8" /> 6 </head> 7 <body th:with="httpStatus=${T(org.springframework.http.HttpStatus).valueOf(#response.status)}"> 8 <h1 th:text="|${httpStatus} - ${httpStatus.reasonPhrase}|">404</h1> 9 <p th:utext="${errorMessage}">Error java.lang.NullPointerException</p> 10 <a href="index.html" th:href="@{/index.html}">返回首页</a> 11 </body> 12 </html>
效果演示
zhangsan 登录
lisi 登录
至此,可以实现基本的权限管理
工程结构
代码已上传至https://github.com/chengjiansheng/cjs-springsecurity-example.git
访问控制表达式
其它
通常情况下登录成功或者失败以后不是跳转到页面而是返回 json 数据,该怎么做呢?
可以继承 SavedRequestAwareAuthenticationSuccessHandler,并在配置中指定 successHandler 或者继承 SimpleUrlAuthenticationFailureHandler,并在配置中指定 failureHandler
1 package com.cjs.example.handler; 2 3 import org.springframework.security.core.Authentication; 4 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 5 6 import javax.servlet.ServletException; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 import java.io.IOException; 10 import java.util.HashMap; 11 12 public class MySavedRequestAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { 13 @Override 14 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { 15 16 // // Use the DefaultSavedRequest URL 17 // String targetUrl = savedRequest.getRedirectUrl(); 18 // logger.debug("Redirecting to DefaultSavedRequest Url:" + targetUrl); 19 // getRedirectStrategy().sendRedirect(request, response, targetUrl); 20 21 Map<String, Object> map = new HashMap<>(); 22 response.getWriter().write(JSON.toJSONString(map)); 23 24 25 } 26 }
这么复杂感觉还不如自己写个 Filter 还简单些
是的,仅仅是这些的话还真不如自己写个过滤器来得简单,但是 Spring Security 的功能远不止如此,比如 OAuth2,CSRF 等等
这个只适用单应用,不可能每个需要权限的系统都这么去写,可以不可以做成认证中心,做单点登录?
当然是可以的,而且必须可以。权限分配可以用一个管理后台,认证和授权必须独立出来,下一节用 OAuth2.0 来实现
参考
https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#el-pre-post-annotations
https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#getting-started
https://www.thymeleaf.org/doc/articles/standarddialect5minutes.html
https://www.thymeleaf.org/doc/articles/layouts.html
https://www.thymeleaf.org/doc/articles/springsecurity.html
https://blog.csdn.net/u283056051/article/details/55803855
https://segmentfault.com/a/1190000008893479
https://www.bbsmax.com/A/A2dmY2DWde/
https://blog.csdn.net/qq_29580525/article/details/79317969
欢迎各位转载,但必须在文章页面中给出作者和原文链接!