1. 简介
1. 概述
Spring是非常流行和成功的Java应用开发框架,Spring Security正是Spring家族中的成员。Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。|
正如你可能知道的关于安全方面的两个主要区域是“认证和授权”(或者访问控制),一般来说,Web应用的安全性包括**用户认证(Authentication)和用户授权( Authonization)**两个部分,这两点也是Spring Security重要核心功能。
- 用户认证指的是∶验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
- 用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
2. 入门案例
1.创建springboot
工程
1.引入依赖
springboot
版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
springcloud
版本
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
书写启动类之后,控制台会打印默认用户 user 的密码
2. 底层过滤器
类 | 用途 |
---|---|
FilterSecurityInterceptor | 最底层的方法级过滤器 |
ExceptionTranslationFilter | 异常过滤器用来处理认证授权过程中抛出的异常 |
UsernamePasswordAuthenticationFilter | 接受POST请求过来的参数,对登录的请求进行拦截,校验表单中的用户名和密码 |
1. 过滤器执行流程
-
使用Spring Security配置过滤器
DelegatingFilterProxy
-
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { Filter delegateToUse = this.delegate; if (delegateToUse == null) { synchronized(this.delegateMonitor) { delegateToUse = this.delegate; if (delegateToUse == null) { WebApplicationContext wac = this.findWebApplicationContext(); if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?"); } delegateToUse = this.initDelegate(wac); } this.delegate = delegateToUse; } } this.invokeDelegate(delegateToUse, request, response, filterChain); }
-
protected Filter initDelegate(WebApplicationContext wac) throws ServletException { String targetBeanName = this.getTargetBeanName(); Assert.state(targetBeanName != null, "No target bean name set"); Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class); if (this.isTargetFilterLifecycle()) { delegate.init(this.getFilterConfig()); } return delegate; }
-
通过FilterChainProxy中的
doFilterInternal
中的List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
获取所有的过滤器加载到过滤链中
3. 两个重要的接口
UserDetailsService
获取数据库中的用户数据PasswordEncoder
加密密码的接口
1. UserDetailsService
- 书写一个类继承
UsernamePasswordAuthenticationFilter
,重写其中的三个方法 - 创建类实现
UserDetailService
,编写查询数据过程,返回User对象(SpringSecurity提供)
2. PasswordEncoder
- 数据加密接口,用于返回User对象里边的密码加密
3. web权限方案
- 主要就是成**认证和授权**
1. 通过配置文件配置
在项目中的
application.yml
中添加以下内容即可
spring:
security:
user:
name: root
password: root
2. 通过配置类来配置
书写的配置类**必须继承
WebSecurityConfigurerAdapter
** , 而且需要返回一个PasswordEncoder的对象,否则就会报错
package com.javacode.config;
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.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhaojinhui
* @date 2020/12/15 11:21
* @apiNote
*/
@Configuration
public class UserConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String password = encoder.encode("admin");
auth.inMemoryAuthentication().withUser("admin").password(password).roles("admin");
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
3. 编写自定义实现类
- 创建配置类,设置使用呢个userDetailsService实现类
- 编写实现类,返回User对象
配置类
package com.javacode.config;
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.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhaojinhui
* @date 2020/12/15 14:57
* @apiNote
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
实现类
package com.javacode.service.impl;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author zhaojinhui
* @date 2020/12/15 15:00
* @apiNote
*/
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User("root", new BCryptPasswordEncoder().encode("admin"), auth);
}
}
4. 连接数据库查询用户角色
1.整合mybatis-plus
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.4.0</version>
</dependency>
<!--数据库相关-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.7</version>
</dependency>
</dependencies>
2. 书写实现类
1. 简单例子 通过用户名查询
- 必须实现
UserDetailsService
接口
如果不配置密码加密器会报错的
package com.javacode.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.javacode.beans.SysUser;
import com.javacode.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author zhaojinhui
* @date 2020/12/15 15:00
* @apiNote
*/
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
wrapper.eq("username", "123");
SysUser sysUser = userMapper.selectOne(wrapper);
if(ObjectUtils.isEmpty(sysUser)){
throw new UsernameNotFoundException("用户名不存在");
}
List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User(sysUser.getUsername(), new BCryptPasswordEncoder().encode(sysUser.getPassword()), auth);
}
}
5. 自定义登录页
1. 配置类新增
package com.javacode.config;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhaojinhui
* @date 2020/12/15 14:57
* @apiNote
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问地址
.defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
.permitAll()
//设置哪些路径可以直接访问不需要认证
.and().authorizeRequests()
.antMatchers("/","/test/hello","/add")
.permitAll()
.and().csrf().disable();//关闭csrf防护
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
2. 前端书写
前端的表单必须叫
username
和password
原因https://www.bilibili.com/video/BV15a411A7kP?p=11&t=112
6. 用户授权
1. hasAuthority
方法
- 如果当前的主体由指定的权限则返回true
- 如果没有对应的权限的话就会报错403
package com.javacode.config;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhaojinhui
* @date 2020/12/15 14:57
* @apiNote
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问地址
.defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
.permitAll()
//设置哪些路径可以直接访问不需要认证
.and().authorizeRequests()
.antMatchers("/","/test/hello")
.permitAll()
.antMatchers("/test/index").hasAnyAuthority("admin")
//这两种都是可以的
//.hasAnyAuthority("admin,user")
//.hasAnyAuthority("admin","user")
.and().csrf().disable()
;//关闭csrf防护
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
2. hasAnyAuthority
- 当前主体有其中一个权限就可以访问
3. hasRole
- 用户的角色都会被添加上
ROLE_
前缀 ,在org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer
中的方法上
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
return "hasRole('ROLE_" + role + "')";
}
}
- 我们的角色设置就需要设置为
"ROLE_admin"
4. hasAnyRole
- 有其中任意一个角色允许访问
7. 自定义403页面
在
config
配置类中修改
package com.javacode.config;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhaojinhui
* @date 2020/12/15 14:57
* @apiNote
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//自定义403页面
http.exceptionHandling().accessDeniedPage("/un_auth.html");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
8. 注解的使用
1. @Secured
用户具有某个角色,可以访问方法
- 需要先启动类添加开启
@EnableGlobalMethodSecurity(securedEnabled = true)
- 在controller中的方法上添加注解,
@Secured("ROLE_admin")
,需要在角色信息前边添加ROLE_
,否则访问了
package com.javacode;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
/**
* @author zhaojinhui
* @date 2020/12/8 22:56
* @apiNote
*/
@SpringBootApplication
@MapperScan("com.javacode.mapper")
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityApp {
public static void main(String[] args) {
SpringApplication.run(SecurityApp.class,args);
}
}
2. @PreAuthorize
使用前需要先开启注解,适用于进入方法前的权限验证,
@PreAuthorize
可以将登录用户的roles/permissions
参数传入方法中
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
/**
* @author zhaojinhui
* @date 2020/12/8 22:56
* @apiNote
*/
@SpringBootApplication
@MapperScan("com.javacode.mapper")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityApp {
public static void main(String[] args) {
SpringApplication.run(SecurityApp.class,args);
}
}
@GetMapping("/delete")
@PreAythorize("hasAnyAuthority('admin')")
public String update(){
return "可以修改";
}
3. @PostAuthorize
使用前先开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
,在方法执行之后验证权限,适用于**验证角色是否有返回的权限**
@PostMapping("submit")
@PostAuthorize("hasAnyAuthority('admins')")
public String sumit(){
return "没有返回权限";
}
4. @PostFilter
对方法返回的数据进行过滤,默认就是filterObject,不需要做出改变
@GetMapping("/getAll")
@PostAuthorize("hasAnyAuthority('admin')")
@PostFilter("filterObject.username == 'admin' ")
public List getAllUser(){
User user2 = new User(“admin”,12);
User user1 = new User(“admin1”,12);
List<User> list = new ArrayList<>();
Collections.addAll(list,user1,user2);
return list;
}
5. @PreFilter
对方法传入的数据进行过滤
6. 权限表达式
9. 用户注销
在配置类中添加
package com.javacode.config;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author zhaojinhui
* @date 2020/12/15 14:57
* @apiNote
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问地址
.defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
.permitAll()
//设置哪些路径可以直接访问不需要认证
.and().authorizeRequests()
.antMatchers("/","/test/hello")
.permitAll()
.antMatchers("/test/index").hasAuthority("admin")
.antMatchers("/test/add").hasAnyRole("add")
.and().csrf().disable()
;//关闭csrf防护
//自定义403页面
http.exceptionHandling().accessDeniedPage("/un_auth.html");
//自定义注销页面
http.logout().logoutUrl("/logout")
.logoutSuccessUrl("/index")
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
10.基于数据库的记住我功能
1.使用cookie
2.安全框架机制实现自动登录原理
第一步: 用户成功登陆之后往浏览器存储一个cookie加密串,然后同样的往数据库中存储包含有用户信息的加密串。
第二部:当用户再次访问的时候,先获取cookie信息用cookie信息到数据库中做对比,如果有对应的信息则认证成功可以登陆
3.具体实现
1. 创建数据库
如果我们不自己创建数据库的话,框架里有自带的建表语句
org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
这个类里边是有的
-- 创建表
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
);
2.修改配置类
- 首先需要注入数据源,配置对象
package com.javacode.config;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* @author zhaojinhui
* @date 2020/12/15 14:57
* @apiNote
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
/** 注入数据源 */
@Autowired
private DataSource dataSource;
/** 配置对象*/
@Bean
public PersistentTokenRepository tokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//如果没有创建表的话需要开启下边的设置
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
- 添加记住我
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问地址
.defaultSuccessUrl("/test/index") //登录成功后的默认跳转路径
.permitAll()
//设置哪些路径可以直接访问不需要认证
.and().authorizeRequests()
.antMatchers("/","/test/hello")
.permitAll()
.antMatchers("/test/index").hasAuthority("admin")
.antMatchers("/test/add").hasAnyRole("add")
.and().rememberMe() //设置记住我功能
.tokenRepository(tokenRepository()) //设置操作token的对象
.tokenValiditySeconds(86400 * 7) //记住七天,此方法以秒为单位
.userDetailsService(userDetailsService) //操作数据库的对象
.and().csrf().disable()
;//关闭csrf防护
//自定义403页面
http.exceptionHandling().accessDeniedPage("/un_auth.html");
//自定义注销页面
http.logout().logoutUrl("/logout")
.logoutSuccessUrl("/index")
.permitAll();
}
3. 前端页面修改
登录的地方添加内容,name的值必须是remember-me
<div>
<label> 记住我:</label>
<input type="checkbox" name="remember-me" title="七天免登陆"/>
</div>
4. 微服务权限方案
1. 什么是微服务
- 微服务的由来
微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
- 微服务优势
(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。
(2)微服务每个模块都可以使用不同的存储方式(比如有的用xedis,有的用mysal.等),数据库也是单个模块对应自己的数据库。。
(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。。
- 微服务本质
(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级。不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。
2. 在微服务中认证和授权的实现过程
1. SSO 单点登录
2. 授权登录
(1)如果是基于Session,那么Spring-security 会对cookie里的sessionid进行解析,找到服务器存储的session信息,然后判断当前用户是否符合请求的要求。
(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限信息中去。
3. 具体实现逻辑
- 数据库设计
-- 权限信息表
CREATE TABLE `acl_permission` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '编号',
`pid` char(19) NOT NULL DEFAULT '' COMMENT '所属上级',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
`type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '类型(1:菜单,2:按钮)',
`permission_value` varchar(50) DEFAULT NULL COMMENT '权限值',
`path` varchar(100) DEFAULT NULL COMMENT '访问路径',
`component` varchar(100) DEFAULT NULL COMMENT '组件路径',
`icon` varchar(50) DEFAULT NULL COMMENT '图标',
`status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限';
-- 角色信息表
CREATE TABLE `acl_role` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '角色id',
`role_name` varchar(20) NOT NULL DEFAULT '' COMMENT '角色名称',
`role_code` varchar(20) DEFAULT NULL COMMENT '角色编码',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 角色权限表
CREATE TABLE `acl_role_permission` (
`id` char(19) NOT NULL DEFAULT '',
`role_id` char(19) NOT NULL DEFAULT '',
`permission_id` char(19) NOT NULL DEFAULT '',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限';
-- 用户信息表
CREATE TABLE `acl_user` (
`id` char(19) NOT NULL COMMENT '会员id',
`username` varchar(20) NOT NULL DEFAULT '' COMMENT '微信openid',
`password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
`salt` varchar(255) DEFAULT NULL COMMENT '用户头像',
`token` varchar(100) DEFAULT NULL COMMENT '用户签名',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 用户角色表
CREATE TABLE `acl_user_role` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '主键id',
`role_id` char(19) NOT NULL DEFAULT '0' COMMENT '角色id',
`user_id` char(19) NOT NULL DEFAULT '0' COMMENT '用户id',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1. 登录认证
- 登录过滤器
package com.eleven.filter;
import com.eleven.entity.SecurityUser;
import com.eleven.entity.User;
import com.eleven.security.TokenManager;
import com.eleven.util.ResponseUtil;
import com.eleven.util.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* @author zhaojinhui
* @date 2021/1/14 23:29
* @apiNote 登录过滤器
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(TokenManager tokenManager,RedisTemplate redisTemplate,AuthenticationManager authenticationManager){
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.authenticationManager = authenticationManager;
//关闭只允许post请求
this.setPostOnly(false);
//设置登录路径和提交方式
this.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/admin/acl/login","POST")
);
}
/**
* 获取表单提交过来的用户名和密码
* @param request http请求
* @param response http响应
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//获取表单提交过来的数据
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(),new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("登录异常");
}
}
/**
* 认证成功之后调用的方法
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//获取用户认真成功的信息
SecurityUser user = (SecurityUser) authResult.getPrincipal();
//根据用户名生成token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
//吧用户名称和权限信息放在redis token中
redisTemplate.opsForValue()
.set(user.getCurrentUserInfo().getUsername(), user.getPermissionList());
//返回token
Map map = new HashMap<>(1);
map.put("token", token);
ResponseUtil.out(response, new Result(map));
}
/**
* 认证失败的调用的方法
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response, new Result().error("认证失败"));
}
}
- 用户退出并删除token
package com.eleven.security;
import cn.hutool.core.util.StrUtil;
import com.eleven.util.ResponseUtil;
import com.eleven.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author zhaojinhui
* @date 2021/1/14 23:03
* @apiNote 用户退出并删除token信息
*/
public class TokenLogoutHandler implements LogoutHandler {
@Autowired
private RedisTemplate redisTemplate;
private TokenManager tokenManager;
public TokenLogoutHandler(TokenManager tokenManager,RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
this.tokenManager = tokenManager;
}
/**
* 用户退出
* @param request 请求
* @param response 响应
* @param authentication
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1.从header中获取token信息
String token = request.getHeader("token");
//2.如果token不为空 移除token。从redis中删除token
if(StrUtil.isNotEmpty(token)){
tokenManager.removeToken(token);
//从token中获取用户信息
String userName = tokenManager.getUserInfo(token);
//从redis中删除用户信息
redisTemplate.delete(userName);
}
ResponseUtil.out(response, new Result());
}
}
2. 添加角色
3. 为角色分配菜单
4. 添加用户
5. 为用户分配角色
3. 完成基于Spring security认证授权案例
mplements LogoutHandler {
@Autowired
private RedisTemplate redisTemplate;
private TokenManager tokenManager;
public TokenLogoutHandler(TokenManager tokenManager,RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
this.tokenManager = tokenManager;
}
/**
* 用户退出
* @param request 请求
* @param response 响应
* @param authentication
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//1.从header中获取token信息
String token = request.getHeader("token");
//2.如果token不为空 移除token。从redis中删除token
if(StrUtil.isNotEmpty(token)){
tokenManager.removeToken(token);
//从token中获取用户信息
String userName = tokenManager.getUserInfo(token);
//从redis中删除用户信息
redisTemplate.delete(userName);
}
ResponseUtil.out(response, new Result());
}
}
#### 2. 添加角色
#### 3. 为角色分配菜单
#### 4. 添加用户
#### 5. 为用户分配角色
## 3. 完成基于Spring security认证授权案例
# 5. 原理总结