在现在web开发中,安全权限的认证一直占着举足轻重的地位,为此Spring自己也出过security安全模块,但是这是一个比较重量级的框架,配置相当的繁琐。后来又出现了shiro这种轻量级的安全框架,里面提供的方法也基本满足开发者的需要。
随着springboot的出现,官方提供了一系列开箱即用的starter,security渐渐重回人们视野,组成了现在常用的springboot+security或者ssm+shiro这样的技术栈。
这里数据库用的是MySQL5.7
/*
SQLyog Ultimate v12.4.3 (64 bit)
MySQL - 5.7.17-log : Database - security
*********************************************************************
*/
/*!40101 SET NAMES utf8 */;
/*!40101 SET SQL_MODE=''*/;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`security` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `security`;
/*Table structure for table `menu` */
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pattern` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `menu` */
insert into `menu`(`id`,`pattern`) values
(1,'/db/**'),
(2,'/admin/**'),
(3,'/user/**');
/*Table structure for table `menu_role` */
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `menu_role` */
insert into `menu_role`(`id`,`mid`,`rid`) values
(1,1,1),
(2,2,2),
(3,3,3);
/*Table structure for table `role` */
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`nameZh` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `role` */
insert into `role`(`id`,`name`,`nameZh`) values
(1,'ROLE_dba','数据库管理员'),
(2,'ROLE_admin','系统管理员'),
(3,'ROLE_user','用户');
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`enabled` tinyint(1) DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `user` */
insert into `user`(`id`,`username`,`password`,`enabled`,`locked`) values
(1,'root','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0),
(2,'admin','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0),
(3,'sang','$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq',1,0);
/*Table structure for table `user_role` */
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
/*Data for the table `user_role` */
insert into `user_role`(`id`,`uid`,`rid`) values
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>leo.study</groupId>
<artifactId>security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml
src/main/resources
org.springframework.boot
spring-boot-maven-plugin
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/security
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
package leo.study.security.bean;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
public class User implements UserDetails
{
private Integer id;
private String username;
private String password;
private Boolean enabled;
private boolean locked;
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
List<SimpleGrantedAuthority> authorities=new ArrayList<>();
for (Role role : roles)
{
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public boolean isAccountNonExpired()
{
return true;
}
@Override
public boolean isAccountNonLocked()
{
return !locked;
}
@Override
public boolean isCredentialsNonExpired()
{
return true;
}
@Override
public boolean isEnabled()
{
return enabled;
}
public Integer getId()
{
return id;
}
public void setId(Integer 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;
}
public void setEnabled(Boolean enabled)
{
this.enabled = enabled;
}
public void setLocked(boolean locked)
{
this.locked = locked;
}
public List<Role> getRoles()
{
return roles;
}
public void setRoles(List<Role> roles)
{
this.roles = roles;
}
}
用户信息表里存放用户登录名、密码、账户锁定、账户可用标志等信息,所以实现UserDetails来保存我们用户的信息。
首先我们要关注的第一个地方是:
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
return null;
}
这里存放的是用户的角色信息,而我们下面呢自己定义了一个role,角色类,所以要把用户对应的角色拿到的话我们需要把role放进user对象里。
这里具体怎么获取用户的角色?
首先创建一个List,因为这个继承来的方法的返回类型是Collection,然后这个集合的泛型是GrantedAuthority的一个继承类SimpleGrantedAuthority,然后开始遍历roles,通过实例化SimpleGrantedAuthority放入角色名称
public SimpleGrantedAuthority(String role) {
Assert.hasText(role, "A granted authority textual representation is required");
this.role = role;
}
第二我们要关注的地方:
我这里数据库里存在enabled和locked字段,而实现的UserDetails也给我们返回了这两个字段。这个是账户是否没有被锁定,注意看Non,所以你要取反,告诉他,没错,没被锁定!其他的我觉得没什么可说的。
@Override
public boolean isAccountNonLocked()
{
return !locked;
}
@Override
public boolean isEnabled()
{
return enabled;
}
package leo.study.security.bean;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
public class Role
{
private Integer id;
private String name;
private String nameZh;
@Override
public String toString()
{
return "Role{" +
"id=" + id +
", name='" + name + '\'' +
", nameZh='" + nameZh + '\'' +
'}';
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getNameZh()
{
return nameZh;
}
public void setNameZh(String nameZh)
{
this.nameZh = nameZh;
}
}
menu类要把角色放进来,待会要根据用户角色看他们对应的菜单
package leo.study.security.bean;
import java.util.List;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
public class Menu
{
private Integer id;
private String pattern;
private List<Role> roles;
@Override
public String toString()
{
return "Menu{" +
"id=" + id +
", pattern='" + pattern + '\'' +
", roles=" + roles +
'}';
}
public List<Role> getRoles()
{
return roles;
}
public void setRoles(List<Role> roles)
{
this.roles = roles;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public String getPattern()
{
return pattern;
}
public void setPattern(String pattern)
{
this.pattern = pattern;
}
}
UserService需要实现UserDetailsService
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
return null;
}
loadUserByUsername顾名思义,通过用户名查询用户信息,首先判断用户是否存在,如果存在判断他具备什么角色,如果不存在则抛出异常提示。
package leo.study.security.service;
import leo.study.security.bean.User;
import leo.study.security.mapper.UserMapper;
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;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
@Service
public class UserService implements UserDetailsService
{
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User user=userMapper.loadUserByUsername(username);
//判断查询结果为空时
if(user==null){
throw new UsernameNotFoundException("用户不存在");
}
user.setRoles(userMapper.getRolesById(user.getId()));
return user;
}
}
package leo.study.security.mapper;
import leo.study.security.bean.Role;
import leo.study.security.bean.User;
import java.util.List;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
public interface UserMapper
{
User loadUserByUsername(String username);
List<Role> getRolesById(Integer id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="leo.study.security.mapper.UserMapper">
<select id="loadUserByUsername" resultType="leo.study.security.bean.User">
select * from user where username=#{username};
</select>
<select id="getRolesById" resultType="leo.study.security.bean.Role">
select * from role where id in(select rid from user_role where uid=#{id});
</select>
</mapper>
package leo.study.security.config;
import leo.study.security.service.UserService;
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.ObjectPostProcessor;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
UserService userService;
//加密
@Bean
PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userService);
}
}
上面的包有些没去掉,没在意。首先BCryptPasswordEncoder是密码加密,现在的security提交的密码必须被加过密,其次就是自己犯的一个小失误@Configuration注解忘记加了,怪不得自己debug半天也找不到问题在哪。
@RestController
public class HelloController
{
@GetMapping("/hello")
public String hello(){
return "hello";
}
security会默认重定向到他的登录页
登陆后请求,没有问题!
当然这是最基本的,我们要实现的是让对应的用户能访问到自己该访问的路径。
之前我们已经准备好了实体类,现在写服务层
package leo.study.security.service;
import leo.study.security.bean.Menu;
import leo.study.security.bean.Role;
import leo.study.security.mapper.MenuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
@Service
public class MenuService
{
@Autowired
MenuMapper menuMapper;
public List<Menu> getAllMenus(){
return menuMapper.getAllMenus();
}
}
package leo.study.security.mapper;
import leo.study.security.bean.Menu;
import java.util.List;
public interface MenuMapper
{
List<Menu> getAllMenus();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="leo.study.security.mapper.MenuMapper">
<resultMap id="BaseResultMap" type="leo.study.security.bean.Menu">
<id property="id" column="id"></id>
<result property="pattern" column="pattern"></result>
<collection property="roles" ofType="leo.study.security.bean.Role">
<id property="id" column="rid"></id>
<result property="name" column="rname"></result>
<result property="nameZh" column="rnameZh"></result>
</collection>
</resultMap>
<select id="getAllMenus" resultMap="BaseResultMap">
select m.*,r.id rid,r.name rname,r.nameZh rnameZh
from menu m left join menu_role mr on m.id=mr.mid left join role r on mr.rid=r.id
</select>
</mapper>
准备完成之后,在配置类里写一个过滤器,作用是获取当前用户的请求地址再与他所有的权限比较,看是否一致。他这里只做比较。待会还会有一个处理类
package leo.study.security.config;
import leo.study.security.bean.Menu;
import leo.study.security.bean.Role;
import leo.study.security.service.MenuService;
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.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/**
* @description:
* 主要功能:
* 分析请求地址
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource
{
//路径匹配
AntPathMatcher antPathMatcher=new AntPathMatcher();
@Autowired
MenuService menuService;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException
{
//获取请求的地址
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> list = menuService.getAllMenus();//获取所有用户角色对应的请求路径
for (Menu menu : list)
{
//开始比较数据库获取的路径与请求路径是否一致
if(antPathMatcher.match(menu.getPattern(),requestUrl)){
//当路径完全一致时,就看这个路径需要哪些角色才能请求
List<Role> roles = menu.getRoles();
//创建一个数组,放置roles
String[] roleStr=new String[roles.size()];
//考虑一个人可能会有多个角色,需要遍历
for (int i = 0; i < roles.size(); i++)
{
//获取角色名
roleStr[i]=roles.get(i).getName();
}
//返回角色对象
return SecurityConfig.createList(roleStr);
}
}
//如果请求的方法,上面方法都没有判断上,那就返回一个ROLE_login标记符,当你请求上面无法
//识别的路径时,自动跳转登陆
return SecurityConfig.createList("ROLE_login");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes()
{
return null;
}
@Override
public boolean supports(Class<?> aClass)
{
return false;
}
}
package leo.study.security.config;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
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;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager
{
//authentication 保存当前用户登陆的信息
//o 获取当前请求对象
//collection public Collection getAttributes(Object o) 的返回值
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException
{
//遍历collection
for (ConfigAttribute attribute : collection)
{
if("ROLE_login".equals(attribute.getAttribute())){
if(authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("非法请求");
}else {
return;
}
}
//这是现在登陆用户所具备的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities)
{
if(authority.getAuthority().equals(attribute.getAttribute())){
return;
}
}
}
throw new AccessDeniedException("非法请求");
}
@Override
public boolean supports(ConfigAttribute configAttribute)
{
return true;
}
@Override
public boolean supports(Class<?> aClass)
{
return true;
}
}
package leo.study.security.config;
import leo.study.security.service.UserService;
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.ObjectPostProcessor;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
/**
* @description:
* @author: Leo
* @createDate: 2020/2/11
* @version: 1.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
UserService userService;
@Autowired
MyFilter myFilter;
@Autowired
MyAccessDecisionManager myAccessDecisionManager;
//加密
@Bean
PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>()
{
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o)
{
o.setSecurityMetadataSource(myFilter);
o.setAccessDecisionManager(myAccessDecisionManager);
return o;
}
}).and().formLogin().permitAll().and().csrf().disable();
}
}
后面会对这个地方做更详细的解释