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依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cjs.example</groupId> <artifactId>cjs-springsecurity-example</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>cjs-springsecurity-example</name> <description></description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> <version>3.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Security配置
package com.cjs.example.config; import com.cjs.example.support.MyUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级别的权限认证 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService myUserDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { // 允许所有用户访问"/"和"/index.html" http.authorizeRequests() .antMatchers("/", "/index.html").permitAll() .anyRequest().authenticated() // 其他地址的访问均需验证权限 .and() .formLogin() .loginPage("/login.html") // 登录页 .failureUrl("/login-error.html").permitAll() .and() .logout() .logoutSuccessUrl("/index.html"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
MyUserDetailsService
package com.cjs.example.support; import com.cjs.example.entity.SysPermission; import com.cjs.example.entity.SysRole; import com.cjs.example.entity.SysUser; import com.cjs.example.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private UserService userService; /** * 授权的时候是对角色授权,而认证的时候应该基于资源,而不是角色,因为资源是不变的,而用户的角色是会变的 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = userService.getUserByName(username); if (null == sysUser) { throw new UsernameNotFoundException(username); } List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (SysRole role : sysUser.getRoleList()) { for (SysPermission permission : role.getPermissionList()) { authorities.add(new SimpleGrantedAuthority(permission.getCode())); } } return new User(sysUser.getUsername(), sysUser.getPassword(), authorities); } }
权限分配
package com.cjs.example.service.impl; import com.cjs.example.dao.UserDao; import com.cjs.example.entity.SysUser; import com.cjs.example.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Cacheable(cacheNames = "authority", key = "#username") @Override public SysUser getUserByName(String username) { return userDao.selectByName(username); } }
package com.cjs.example.dao; import com.cjs.example.entity.SysPermission; import com.cjs.example.entity.SysRole; import com.cjs.example.entity.SysUser; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Repository; import java.util.Arrays; @Slf4j @Repository public class UserDao { private SysRole admin = new SysRole("ADMIN", "管理员"); private SysRole developer = new SysRole("DEVELOPER", "开发者"); { SysPermission p1 = new SysPermission(); p1.setCode("UserIndex"); p1.setName("个人中心"); p1.setUrl("/user/index.html"); SysPermission p2 = new SysPermission(); p2.setCode("BookList"); p2.setName("图书列表"); p2.setUrl("/book/list"); SysPermission p3 = new SysPermission(); p3.setCode("BookAdd"); p3.setName("添加图书"); p3.setUrl("/book/add"); SysPermission p4 = new SysPermission(); p4.setCode("BookDetail"); p4.setName("查看图书"); p4.setUrl("/book/detail"); admin.setPermissionList(Arrays.asList(p1, p2, p3, p4)); developer.setPermissionList(Arrays.asList(p1, p2)); } public SysUser selectByName(String username) { log.info("从数据库中查询用户"); if ("zhangsan".equals(username)) { SysUser sysUser = new SysUser("zhangsan", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm"); sysUser.setRoleList(Arrays.asList(admin, developer)); return sysUser; }else if ("lisi".equals(username)) { SysUser sysUser = new SysUser("lisi", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm"); sysUser.setRoleList(Arrays.asList(developer)); return sysUser; } return null; } }
示例
这里我设计的例子是用户登录成功以后跳到个人中心,然后用户可以可以进入图书列表查看。
用户zhangsan可以查看所有的,而lisi只能查看图书列表,不能添加不能查看详情。
页面设计
LoginController.java
package com.cjs.example.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class LoginController { // Login form @RequestMapping("/login.html") public String login() { return "login.html"; } // Login form with error @RequestMapping("/login-error.html") public String loginError(Model model) { model.addAttribute("loginError", true); return "login.html"; } }
BookController.java
package com.cjs.example.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/book") public class BookController { @PreAuthorize("hasAuthority(‘BookList‘)") @GetMapping("/list.html") public String list() { return "book/list"; } @PreAuthorize("hasAuthority(‘BookAdd‘)") @GetMapping("/add.html") public String add() { return "book/add"; } @PreAuthorize("hasAuthority(‘BookDetail‘)") @GetMapping("/detail.html") public String detail() { return "book/detail"; } }
UserController.java
package com.cjs.example.controller; import com.cjs.example.entity.SysUser; import com.cjs.example.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; /** * 个人中心 */ @PreAuthorize("hasAuthority(‘UserIndex‘)") @GetMapping("/index") public String index() { return "user/index"; } @RequestMapping("/hi") @ResponseBody public String hi() { SysUser sysUser = userService.getUserByName("zhangsan"); return sysUser.toString(); } }
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h2>这里是首页</h2> </body> </html>
login.html
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Login page</title> </head> <body> <h1>Login page</h1> <p th:if="${loginError}" class="error">用户名或密码错误</p> <form th:action="@{/login.html}" method="post"> <label for="username">Username</label>: <input type="text" id="username" name="username" autofocus="autofocus" /> <br /> <label for="password">Password</label>: <input type="password" id="password" name="password" /> <br /> <input type="submit" value="Login" /> </form> </body> </html>
/user/index.html
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>个人中心</title> </head> <body> <h2>个人中心</h2> <div th:insert="~{fragments/header::logout}"></div> <a href="/book/list.html">图书列表</a> </body> </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
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <body> <div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> Logged in user: <span sec:authentication="name"></span> | Roles: <span sec:authentication="principal.authorities"></span> <div> <form action="#" th:action="@{/logout}" method="post"> <input type="submit" value="退出" /> </form> </div> </div> </body> </html>
错误处理
ErrorController.java
package com.cjs.example.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @Slf4j @ControllerAdvice public class ErrorController { @ExceptionHandler(Throwable.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String exception(final Throwable throwable, final Model model) { log.error("Exception during execution of SpringSecurity application", throwable); String errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error"); model.addAttribute("errorMessage", errorMessage); return "error"; } }
error.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Error page</title> <meta charset="utf-8" /> </head> <body th:with="httpStatus=${T(org.springframework.http.HttpStatus).valueOf(#response.status)}"> <h1 th:text="|${httpStatus} - ${httpStatus.reasonPhrase}|">404</h1> <p th:utext="${errorMessage}">Error java.lang.NullPointerException</p> <a href="index.html" th:href="@{/index.html}">返回首页</a> </body> </html>
效果演示
zhangsan登录
lisi登录
至此,可以实现基本的权限管理
工程结构
代码已上传至https://github.com/chengjiansheng/cjs-springsecurity-example.git
访问控制表达式
其它
通常情况下登录成功或者失败以后不是跳转到页面而是返回json数据,该怎么做呢?
可以继承SavedRequestAwareAuthenticationSuccessHandler,并在配置中指定successHandler或者继承SimpleUrlAuthenticationFailureHandler,并在配置中指定failureHandler
package com.cjs.example.handler; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; public class MySavedRequestAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // // Use the DefaultSavedRequest URL // String targetUrl = savedRequest.getRedirectUrl(); // logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl); // getRedirectStrategy().sendRedirect(request, response, targetUrl); Map<String, Object> map = new HashMap<>(); response.getWriter().write(JSON.toJSONString(map)); } }
这么复杂感觉还不如自己写个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