简介
这一小节主要是,整合 Spring Security
步骤
-
新建数据库与表
Create DATABASE `backend_template`; USE backend_template; CREATE TABLE `user` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `role` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `user_role` ( `user_id` bigint(11) NOT NULL, `role_id` bigint(11) NOT NULL ); CREATE TABLE `role_permission` ( `role_id` bigint(11) NOT NULL, `permission_id` bigint(11) NOT NULL ); CREATE TABLE `permission` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `url` varchar(255) NOT NULL, `name` varchar(255) NOT NULL, `description` varchar(255) NULL, `pid` bigint(11) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e'); INSERT INTO user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e'); INSERT INTO role (id, name) VALUES (1,'USER'); INSERT INTO role (id, name) VALUES (2,'ADMIN'); INSERT INTO permission (id, url, name, pid) VALUES (1,'/user/common','common',0); INSERT INTO permission (id, url, name, pid) VALUES (2,'/user/admin','admin',0); INSERT INTO user_role (user_id, role_id) VALUES (1, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 2); INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
-
修改
application-dev.properties
配置文件中连接数据库为backend_template
,此时就可以把test数据库删了,因为第三篇还要连接所以之前没让删spring.datasource.druid.url=jdbc:mysql://127.0.0.1:3306/backend_template?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
-
在
pom.xml
中新增以下两个依赖<!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>2.3.1.RELEASE</version> </dependency> <!-- thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>2.3.1.RELEASE</version> </dependency>
-
在com.example.backend_template.entity下新建User类
package com.example.backend_template.entity; import org.springframework.security.core.userdetails.UserDetails; import java.io.Serializable; import java.util.List; /** * @ClassName User * @Description * @Author L * @Date Create by 2020/6/28 */ public class User implements UserDetails, Serializable { private Long id; private String username; private String password; private List<Role> authorities; public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public List<Role> getAuthorities() { return authorities; } public void setAuthorities(List<Role> authorities) { this.authorities = authorities; } /** * 用户帐号是否未过期 * * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 用户帐号是否未锁定 * * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 用户密码是否未过期 * * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 用户是否可用 * * @return */ @Override public boolean isEnabled() { return true; } }
上面的 User 类实现了 UserDetails 接口,该接口是实现Spring Security 认证信息的核心接口。其中 getUsername 方法为 UserDetails 接口 的方法,这个方法返回 username,也可以是其他的用户信息,例如手机号、邮箱等。getAuthorities() 方法返回的是该用户设置的权限信息,在本实例中,从数据库取出用户的所有角色信息,权限信息也可以是用户的其他信息,不一定是角色信息。另外需要读取密码,最后几个方法一般情况下都返回 true,也可以根据自己的需求进行业务判断。
-
在com.example.backend_template.entity下新建Role类
package com.example.backend_template.entity; import org.springframework.security.core.GrantedAuthority; /** * @ClassName Role * @Description * @Author L * @Date Create by 2020/6/28 */ public class Role implements GrantedAuthority { private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String getAuthority() { return name; } }
Role 类实现了 GrantedAuthority 接口,并重写 getAuthority() 方法。权限点可以为任何字符串,不一定非要用角色名。
AuthenticationManager会设置到一个GrantedAuthority列表到Authentication对象中保存,GrantedAuthority列表表示用户所具有的权限,AccessDecisionManager将从Authentication中获取用户的GrantedAuthority来鉴定用户是否具有访问权限。 -
在com.example.backend_template.dao下新建UserDao接口
package com.example.backend_template.dao; import com.example.backend_template.entity.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; /** * @ClassName UserDao * @Description * @Author L * @Date Create by 2020/6/28 */ @Mapper public interface UserDao { /** * 通过用户名加载用户 * * @param userName * @return */ User findByUsername(@Param("userName") String userName); }
-
在
resources/mapper
下新建UserDao.xml文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.backend_template.dao.UserDao"> <select id="findByUsername" parameterType="string" resultType="com.example.backend_template.entity.User"> SELECT * FROM user u WHERE u.username = #{userName} </select> </mapper>
-
在com.example.backend_template.dao下新建RoleDao接口
package com.example.backend_template.dao; import com.example.backend_template.entity.Role; import org.apache.ibatis.annotations.Mapper; import org.springframework.data.repository.query.Param; import java.util.List; /** * @ClassName RoleDao * @Description * @Author L * @Date Create by 2020/6/28 */ @Mapper public interface RoleDao { /** * 通过用户ID查找用户角色 * * @param userId * @return */ List<Role> findByUserId(@Param("userId") Long userId); }
-
在
resources/mapper
下新建RoleDao.xml文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.backend_template.dao.RoleDao"> <select id="findByUserId" parameterType="Long" resultType="com.example.backend_template.entity.Role"> SELECT * FROM role r WHERE r.id IN (SELECT ur.role_id FROM user_role ur WHERE ur.user_id = #{userId} ) </select> </mapper>
-
在com.example.backend_template.dao下新建PermissionDao接口
package com.example.backend_template.dao; import org.apache.ibatis.annotations.Mapper; import java.util.List; import java.util.Map; /** * @ClassName PermissionDao * @Description * @Author L * @Date Create by 2020/6/29 */ @Mapper public interface PermissionDao { List<Map<String, String>> findRoleAndPermissions(); }
-
在
resources/mappers
下新建PermissionDao.xml文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.backend_template.dao.PermissionDao"> <select id="findRoleAndPermissions" resultType="java.util.HashMap"> SELECT R.name,P.url FROM role AS R LEFT JOIN role_permission RP ON R.id=RP.role_id LEFT JOIN permission AS P ON RP.permission_id=P.id </select> </mapper>
-
并在
BackendTemplateApplication
启动类中添加@MapperScan("com.example.backend_template.dao")
注解,如之前未删除则不再添加 -
在com.example.backend_template.security下新建UserDetailsServiceImpl类
package com.example.backend_template.security; import com.example.backend_template.dao.RoleDao; import com.example.backend_template.dao.UserDao; import com.example.backend_template.entity.Role; import com.example.backend_template.entity.User; import org.springframework.beans.factory.annotation.Autowired; 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.List; /** * @ClassName UserDetailsService * @Description * @Author L * @Date Create by 2020/6/29 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserDao userDao; @Autowired private RoleDao roleDao; @Override public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { //查数据库,查找到用户名对应的所有角色,并注入user中 User user = userDao.findByUsername(userName); if (user != null) { List<Role> roles = roleDao.findByUserId(user.getId()); user.setAuthorities(roles); } return user; } }
-
在com.example.backend_template.security下新建InvocationSecurityMetadataSourceServiceImpl类
package com.example.backend_template.security; import com.example.backend_template.dao.PermissionDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.*; /** * @ClassName InvocationSecurityMetadataSourceServiceImpl * @Description * @Author L * @Date Create by 2020/6/29 */ @Component public class InvocationSecurityMetadataSourceServiceImpl implements FilterInvocationSecurityMetadataSource { @Autowired private PermissionDao permissionDao; /** * 每一个资源所需要的角色 Collection<ConfigAttribute>决策器会用到 */ private static HashMap<String, Collection<ConfigAttribute>> map = null; /** * 返回请求的资源需要的角色 * * @param o * @return * @throws IllegalArgumentException */ @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { //object 中包含用户请求的request 信息 HttpServletRequest request = ((FilterInvocation) o).getHttpRequest(); for (Iterator<String> it = map.keySet().iterator(); it.hasNext(); ) { String url = it.next(); if (new AntPathRequestMatcher(url).matches(request)) { return map.get(url); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { //初始化 所有资源 对应的角色 loadResourceDefine(); return null; } @Override public boolean supports(Class<?> aClass) { return true; } /** * 初始化 所有资源 对应的角色 */ public void loadResourceDefine() { map = new HashMap<>(16); //查出结果为角色和对应URL的集合 List<Map<String, String>> roleAndPermissions = permissionDao.findRoleAndPermissions(); //某个资源可以被哪些角色访问 for (Map<String, String> roleAndPermission : roleAndPermissions) { String roleName = roleAndPermission.get("name"); String url = roleAndPermission.get("url"); ConfigAttribute role = new SecurityConfig(roleName); if (map.containsKey(url)) { map.get(url).add(role); } else { List<ConfigAttribute> list = new ArrayList<>(); list.add(role); map.put(url, list); } } } }
InvocationSecurityMetadataSourceServiceImpl 类实现了 FilterInvocationSecurityMetadataSource,FilterInvocationSecurityMetadataSource 的作用是用来储存请求与权限的对应关系。
FilterInvocationSecurityMetadataSource接口有3个方法:
boolean supports(Class<?> clazz):指示该类是否能够为指定的方法调用或Web请求提供ConfigAttributes。
Collection getAllConfigAttributes():Spring容器启动时自动调用, 一般把所有请求与权限的对应关系也要在这个方法里初始化, 保存在一个属性变量里。
Collection getAttributes(Object object):当接收到一个http请求时, filterSecurityInterceptor会调用的方法. 参数object是一个包含url信息的HttpServletRequest实例. 这个方法要返回请求该url所需要的所有权限集合 -
在com.example.backend_template.security下新建AccessDecisionManagerImpl类
package com.example.backend_template.security; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.Iterator; /** * @ClassName AccessDecisionManagerImpl * @Description * @Author L * @Date Create by 2020/6/30 */ @Component public class AccessDecisionManagerImpl implements AccessDecisionManager { /** * 通过传递的参数来决定用户是否有访问对应受保护对象的权限 * * @param authentication 包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时UserDetailsService中设置的authorities。 * @param object 就是FilterInvocation对象,可以得到request等web资源 * @param configAttributes configAttributes是本次访问需要的权限 */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if (configAttributes == null || configAttributes.size() <= 0) { return; } else { String needRole; for (Iterator<ConfigAttribute> iterator = configAttributes.iterator(); iterator.hasNext(); ) { needRole = iterator.next().getAttribute(); for (GrantedAuthority ga : authentication.getAuthorities()) { if (needRole.trim().equals(ga.getAuthority().trim())) { return; } } } } throw new AccessDeniedException("当前没有访问权限!"); } /** * 表示此AccessDecisionManager是否能够处理传递的ConfigAttribute呈现的授权请求 * * @param configAttribute * @return */ @Override public boolean supports(ConfigAttribute configAttribute) { return true; } /** * 表示当前AccessDecisionManager实现是否能够为指定的安全对象(方法调用或Web请求)提供访问控制决策 * * @param aClass * @return */ @Override public boolean supports(Class<?> aClass) { return true; } }
AccessDecisionManagerImpl 类实现了AccessDecisionManager接口,AccessDecisionManager是由AbstractSecurityInterceptor调用的,它负责鉴定用户是否有访问对应资源(方法或URL)的权限。
-
在com.example.backend_template.security下新建FilterSecurityInterceptorImpl类
package com.example.backend_template.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.SecurityMetadataSource; import org.springframework.security.access.intercept.AbstractSecurityInterceptor; import org.springframework.security.access.intercept.InterceptorStatusToken; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import javax.servlet.*; import java.io.IOException; /** * @ClassName FilterSecurityInterceptorImpl * @Description * @Author L * @Date Create by 2020/6/30 */ @Component public class FilterSecurityInterceptorImpl extends AbstractSecurityInterceptor implements Filter { @Autowired private FilterInvocationSecurityMetadataSource securityMetadataSource; @Autowired public void setAccessDecisionManagerImpl(AccessDecisionManagerImpl accessDecisionManagerImpl) { super.setAccessDecisionManager(accessDecisionManagerImpl); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain); invoke(fi); } public void invoke(FilterInvocation fi) throws IOException, ServletException { InterceptorStatusToken token = super.beforeInvocation(fi); try { //执行下一个拦截器 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } }
每种受支持的安全对象类型(方法调用或Web请求)都有自己的拦截器类,它是AbstractSecurityInterceptor的子类,AbstractSecurityInterceptor 是一个实现了对受保护对象的访问进行拦截的抽象类。
AbstractSecurityInterceptor中的方法说明:
beforeInvocation()方法实现了对访问受保护对象的权限校验,内部用到了AccessDecisionManager和AuthenticationManager;
finallyInvocation()方法用于实现受保护对象请求完毕后的一些清理工作,主要是如果在beforeInvocation()中改变了SecurityContext,则在finallyInvocation()中需要将其恢复为原来的SecurityContext,该方法的调用应当包含在子类请求受保护资源时的finally语句块中。
afterInvocation()方法实现了对返回结果的处理,在注入了AfterInvocationManager的情况下默认会调用其decide()方法。
了解了AbstractSecurityInterceptor,就应该明白了,我们自定义FilterSecurityInterceptorImpl就是想使用我们之前自定义的 AccessDecisionManager 和 securityMetadataSource。 -
在com.example.backend_template.security下新建SecurityConfig类
package com.example.backend_template.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.password.PasswordEncoder; import org.springframework.util.DigestUtils; /** * @ClassName SecurityConfig * @Description * @Author L * @Date Create by 2020/6/30 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userService; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { //校验用户 auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() { //对密码进行加密 @Override public String encode(CharSequence charSequence) { return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); } //对密码进行判断匹配 @Override public boolean matches(CharSequence charSequence, String s) { String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); boolean res = s.equals(encode); return res; } }); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/index", "/login", "/login-error", "/401").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login").failureUrl("/login-error") .and() .exceptionHandling().accessDeniedPage("/401"); http.logout().logoutSuccessUrl("/"); } }
@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security。自定义类继承了WebSecurityConfigurerAdapter来重写了一些方法来指定一些特定的Web安全设置
测试
- 在com.example.backend_template.controller下新建SecurityController类
package com.example.backend_template.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; /** * @ClassName SecurityController * @Description * @Author L * @Date Create by 2020/6/30 */ @Controller public class SecurityController { @RequestMapping("/") public String root() { return "redirect:/index"; } @RequestMapping("/index") public String index() { return "index"; } @RequestMapping("/login") public String login() { return "login"; } @RequestMapping("/login-error") public String loginError(Model model) { model.addAttribute( "loginError" , true); return "login"; } @GetMapping("/401") public String accessDenied() { return "401"; } @GetMapping("/user/common") public String common() { return "user/common"; } @GetMapping("/user/admin") public String admin() { return "user/admin"; } }
- 在
resources/templates
下新建401.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>401 page</title> </head> <body> <div> <div> <h2>权限不够</h2> <p>拒绝访问!</p> </div> </div> </body> </html>
- 在
resources/templates
下新建index.html<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h2>page list</h2> <a href="/user/common">common page</a> <br/> <a href="/user/admin">admin page</a> <br/> <form th:action="@{/logout}" method="post"> <input type="submit" class="btn btn-primary" value="注销"/> </form> </body> </html>
- 在
resources/templates
下新建login.html<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <h1>Login page</h1> <p th:if="${loginError}" class="error">用户名或密码错误</p> <form th:action="@{/login}" method="post"> <label for="username">用户名</label>: <input type="text" id="username" name="username" autofocus="autofocus" /> <br/> <label for="password">密 码</label>: <input type="password" id="password" name="password" /> <br/> <input type="submit" value="登录" /> </form> <p><a href="/index" th:href="@{/index}"></a></p> </body> </html>
- 首先在resources/templates下新建user文件夹,然后在
resources/templates/user
下新建admin.html<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>admin page</title> </head> <body> success admin page!!! </body> </html>
- 在
resources/templates/user
下新建common.html<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>common page</title> </head> <body> success common page!!! </body> </html>
- 启动项目,并用浏览器访问 http://localhost:8080/index 并选择
common page
- 输入用户名:”user“,密码:”123456“,出现以下界面,说明登陆成功
- 接着访问 http://localhost:8080/user/admin ,会出现以下界面,因为user用户没有权力访问
/user/admin
界面,必须用admin
帐号才能访问
如成功出现以上所有结果,则Spring Security 整合成功
项目地址
项目介绍:从零搭建 Spring Boot 后端项目
代码地址:https://github.com/xiaoxiamo/backend-template