今天的主要任务是整合 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中新增以下两个依赖
org.springframework.boot
spring-boot-starter-security
2.3.1.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
2.3.1.RELEASE
在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;
public class User implements UserDetails, Serializable {
private Long id;
private String username;
private String password;
private List 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 getAuthorities() {
return authorities;
}
public void setAuthorities(List authorities) {
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@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;
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;
@Mapper
public interface UserDao {
User findByUsername(@Param("userName") String userName);
}
在resources/mapper下新建UserDao.xml文件
SELECT * FROM user u WHERe u.username = #{userName}
在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;
@Mapper
public interface RoleDao {
List findByUserId(@Param("userId") Long userId);
}
在resources/mapper下新建RoleDao.xml文件
在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;
@Mapper
public interface PermissionDao {
List
在resources/mappers下新建PermissionDao.xml文件
并在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;
@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 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.*;
@Component
public class InvocationSecuritymetadataSourceServiceImpl implements FilterInvocationSecuritymetadataSource {
@Autowired
private PermissionDao permissionDao;
private static HashMap> map = null;
@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
//object 中包含用户请求的request 信息
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for (Iterator 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 getAllConfigAttributes() {
//初始化 所有资源 对应的角色
loadResourceDefine();
return null;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
public void loadResourceDefine() {
map = new HashMap<>(16);
//查出结果为角色和对应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;
@Component
public class AccessDecisionManagerImpl implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (configAttributes == null || configAttributes.size() <= 0) {
return;
} else {
String needRole;
for (Iterator 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("当前没有访问权限!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@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;
@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;
@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;
@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
401 page
权限不够
拒绝访问!
在resources/templates下新建index.html
首页
page list
common page
admin page
在resources/templates下新建login.html
登录
Login page
用户名或密码错误
首先在resources/templates下新建user文件夹,然后在resources/templates/user下新建admin.html
admin page
success admin page!!!
在resources/templates/user下新建common.html
common page
success common page!!!


