前言
在現在web開發中,安全權限的認證一直佔着舉足輕重的地位,爲此Spring自己也出過security安全模塊,但是這是一個比較重量級的框架,配置相當的繁瑣。後來又出現了shiro這種輕量級的安全框架,裏面提供的方法也基本滿足開發者的需要。
隨着springboot的出現,官方提供了一系列開箱即用的starter,security漸漸重回人們視野,組成了現在常用的springboot+security或者ssm+shiro這樣的技術棧。
SQL腳本
這裏數據庫用的是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</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
連接數據庫
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
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
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;
}
}
對應mapper類
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);
}
mapper的配置文件
<?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會默認重定向到他的登錄頁
登陸後請求,沒有問題!
當然這是最基本的,我們要實現的是讓對應的用戶能訪問到自己該訪問的路徑。
查詢對應用戶能訪問的菜單路徑
之前我們已經準備好了實體類,現在寫服務層
menuservice
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();
}
}
MenuMapper
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;
}
}
MyAccessDecisionManager
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<ConfigAttribute> 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();
}
}
後面會對這個地方做更詳細的解釋