Spring Boot Security
前言
-
Spring Boot Security
是一套安全框架 -
它能帮你做两件事:认证、授权
-
底层实现就是一个过滤器链
责任链模式
,每个过滤器链都只做一件事 -
过滤器包括:
-
WebAsyncManagerIntegrationFilter
:将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager 进行集成 -
SecurityContextPersistenceFilter
:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信 息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除 例如在Session中维护一个用户的安全信息就是这个过滤器处理的 -
HeaderWriterFilter
:用于将头信息加入响应中 -
CsrfFilter
:用于处理跨站请求伪造 -
LogoutFilter
:用于处理退出登录 -
UsernamePasswordAuthenticationFilter
:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自“/login”的请求。从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改 -
DefaultLoginPageGeneratingFilter
:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面 -
BasicAuthenticationFilter
:检测和处理http basic认证 -
RequestCacheAwareFilter
:主要是包装请求对象request -
AnonymousAuthenticationFilter
:检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication -
SessionManagementFilter
:管理session的过滤器 -
ExceptionTranslationFilter
:处理 AccessDeniedException 和 AuthenticationException 异常 -
FilterSecurityInterceptor
:可以看做过滤器链的出口 -
RememberMeAuthenticationFilter
:当用户没有登录而直接访问资源时, 从cookie里找出用户的信息, 如果Spring Security能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启 -
责任链的客户类是
HttpSecurity
, -
它负责对责任链的创建和管理,它的 addFilterAt(Filter filter, Class atFilter) 方法可在责任链中添加一个过滤器。
-
在这个框架中 过滤器作为了 抽象处理者Handler的角色,各个具体的过滤器类是 具体处理者Concrete Handler角色 ,HttpSecueiry是 客户类角色
入门
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
-
引入后即刻生效,默认拦截所有请求,直接重置到登录页
-
启动后默认账号user,默认密码会在控制台输出
2. 配置类
-
继承WebSecurityConfigurerAdapter适配器,重写配置方法
-
用户认证相关功能
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
//配置用户
.withUser("admin").password("admin").roles("p1")
.and()
.withUser("chai").password("123").authorities("p2")
.and()
//配置密码编码器,此处使用不编码
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
- 这种写死的账号密码也可以写到配置文件中application.yml
spring:
security:
user:
name: admin
password: admin
roles: admin
- 权限控制相关功能
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭跨域限制
.authorizeRequests() //以下权限按顺序开始认证匹配,匹配到就不继续往下
.antMatchers("/r/r1").hasAnyRole("p1") //访问/r/r1接口路由,需要拥有p1角色
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated() //r/下的所有资源需要登录
.anyRequest().permitAll() //其他请求放行
.and()
.formLogin(); //打开表单登录,一旦重写了配置类,原默认表单登录就失效了,需要手动打开
}
3. 测试
-
写个控制器进行测试
-
TestController
@RestController
public class TestController {
@GetMapping("/t")
public String t(){
return "访问t资源!";
}
@GetMapping("/r/r1")
public String r1(){
return "访问r1资源";
}
@GetMapping("/r/r2")
public String r2(){
return "访问r2资源";
}
}
- 启动项目,访问
http://localhost:8080/t
,访问成功,无拦截
- 访问
http://localhost:8080/r/r1
,被重定向到了登录页,输入admin/admin登录
- 访问
http://localhost:8080/r/r2
,无权访问
进价
- 后面自定义的配置,基本全是在修改WebSecurityConfigurerAdapter适配器的配置类
1. 自定义登录页
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭跨域限制
.authorizeRequests() //以下权限按顺序开始认证匹配,匹配到就不继续往下
.antMatchers("/r/r1").hasAnyRole("p1") //访问/r/r1接口路由,需要拥有p1角色
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated() //r/下的所有资源需要登录
.anyRequest().permitAll() //其他请求放行
.and()
.formLogin() //允许表单提交
.loginPage("/login.html")//自定义登录页,也可以重定向到接口.loginPage("/login")
.loginProcessingUrl("/loginAction"); //自定义登录表单提交的地址,与前端的form-action对应,默认Post
}
- 前端:resources/static/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/loginAction" method="post">
<div>
用户名:<input type="text" name="username"/>
</div>
<div>
密码:<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="登录"/>
</div>
</form>
</body>
</html>
- 启动项目,访问
http://localhost:8080/login.html
输入admin/admin 登录成功
-
其他登录相关配置项
-
successForwardUrl("/login-success")
:登录成功后转发到接口 -
defaultSuccessUrl("/home.html", true):
登录成功后默认的返回视图,这里也可以是视图控制器的接口 -
failureForwardUrl()
: 登录失败转发到接口 -
failureUrl("/login.html?error")
:登录失败后返回视图 -
logoutUrl("/custom-logout")
:默认注销行为为logout -
logoutSuccessUrl("")
:设置注销成功后跳转页面,默认是跳转到登录页面 -
usernameParameter("username")
:配置前端用户名字段名,默认:username
-
passwordParameter("password")
:配置前端密码字段名,默认:password
2. 用户认证从数据库中获取
- 入门里用户都是写死的,实际项目中应该是动态从数据库中进行用户认证与权限查询
2.1重写加载用户逻辑
-
创建一个类
UserDetailsServiceImpl
-
实现
org.springframework.security.core.userdetails.UserDetailsService
接口,重写loadUserByUsername
方法
import com.google.common.collect.Lists;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
/**
* 先写个最简化版本
* 后续去数据库里查询用户就是在这里写
**/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//组装用户权限,对应配置文件里的.hasAnyAuthority("p1"),如果要对应.hasAnyRole("p2"),这里的p1需要写成ROLE_p1
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("p1");
//返回的这个User需要三个参数,用户名,密码,权限;这里返回的用户密码会和前端输入的用户名密码进行比对。
return new User("admin","123456",grantedAuthorities);
}
}
-
启动服务,输入admin/123456发现可以正常登陆,并且能访问
/r/r1
资源 -
重写
loadUserByUsername
这个方法主要是给密码验证器DaoAuthenticationProvider
用的,也可以通过继承DaoAuthenticationProvider
,重写里面的方法,进行自定义密码验证,这里不演示了 -
注入配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//注入自定义的用户加载类
auth.userDetailsService(userDetailsService);
}
/**
*与上面configure注入userDetailsService一样,不同的写法,任选其一
* @Bean
* public UserDetailsService userDetailsService(){
* return new UserDetailsServiceImpl();
* }
*/
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance(); //先不加密
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/r/r2").hasAnyRole("p2") //会自动加前缀 ROLE_
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginAction");
}
}
hasAnyRole
源码,发现自动增加了前缀,所以如果用这个方法校验权限,需要在用户权限组装那加上ROLE_
前缀
2.2用户从数据库中读取
- 添加数据库依赖,
mysql
数据库、Mybatis-plus
的ORM框架、Hikari
连接池
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatisplus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!--连接池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
application.yml
配置数据库连接
spring:
datasource:
url: jdbc:mysql://localhost:3306/czadmin?useUnicode=true&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
mybatis-plus:
typeAliasesPackage: com.chai.entity
configuration:
map-underscore-to-camel-case: true
call-setters-on-nulls: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
-
创建数据库
czadmin
,创建表结构 -
sys_user
表
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`dept_id` int(11) DEFAULT NULL COMMENT '部门名称',
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户名',
`nick_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
`gender` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '性别',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '手机号码',
`email` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '邮箱',
`avatar_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像地址',
`avatar_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '头像磁盘路径',
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
`is_admin` tinyint(1) DEFAULT 0 COMMENT '是否超管',
`enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建操作人',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '更新操作人',
`pwd_reset_time` datetime DEFAULT NULL COMMENT '修改密码的时间',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE KEY `idx_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='系统用户';
-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 2, 'admin', '管理员', '男', '18888888888', '201507802@qq.com', 'avatar-20200806032259161.png', '/Users/jie/Documents/work/me/admin/eladmin/~/avatar/avatar-20200806032259161.png', '$2a$10$Egp1/gvFlt7zhlXVfEFw4OfWQCGPw0ClmMcc6FjTnvXNRVf9zdMRa', 1, 1, NULL, 'admin', '2020-05-03 16:38:31', '2018-08-23 09:11:56', '2020-09-05 10:43:31');
INSERT INTO `sys_user` VALUES (2, 2, 'test', '测试', '男', '19999999999', '231@qq.com', NULL, NULL, '$2a$10$4XcyudOYTSz6fue6KFNMHeUQnCX5jbBQypLEnGk1PmekXt5c95JcK', 0, 1, 'admin', 'admin', NULL, '2020-05-05 11:15:49', '2020-09-05 10:43:38');
COMMIT;
- 重写
loadUserByUsername
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//通过用户名查询用户信息
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, userName));
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户不存在");
}
//查询用户角色权限信息,此处写死了,下面处理
List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("p1");
return new User(user.getUsername(), user.getPassword(), user.getEnabled(), true, true, true, grantedAuthorities);
}
}
- 用户从数据库中读取完成,密码校验交给security后续处理,接下来处理权限
2.3权限从数据库中获取
-
创建表结构
-
sys_menu
菜单表,权限表与菜单表合一
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`menu_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`pid` int(11) DEFAULT NULL COMMENT '上级菜单ID',
`menu_type` int(2) DEFAULT NULL COMMENT '菜单类型,0:菜单1:路由2:按钮',
`title` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '菜单标题',
`name` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '组件名称',
`component` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '组件',
`menu_sort` int(5) DEFAULT NULL COMMENT '排序',
`icon` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '图标',
`path` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '链接地址',
`i_frame` tinyint(1) DEFAULT '0' COMMENT '是否外链',
`cache` tinyint(1) DEFAULT '0' COMMENT '缓存',
`hidden` tinyint(1) DEFAULT '0' COMMENT '隐藏',
`permission` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '权限',
`create_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '创建操作人',
`update_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '更新操作人',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='系统菜单';
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
BEGIN;
INSERT INTO `sys_menu` VALUES (1, NULL, 0, '系统管理', NULL, NULL, 1, 'system', 'system', 0, 0, 0, NULL, NULL, NULL, '2018-12-18 15:11:29', NULL);
INSERT INTO `sys_menu` VALUES (2, 1, 1, '用户管理', 'User', 'system/user/index', 2, 'peoples', 'user', 0, 0, 0, 'user:list', NULL, NULL, '2018-12-18 15:14:44', NULL);
INSERT INTO `sys_menu` VALUES (3, 1, 1, '角色管理', 'Role', 'system/role/index', 3, 'role', 'role', 0, 0, 0, 'roles:list', NULL, NULL, '2018-12-18 15:16:07', NULL);
INSERT INTO `sys_menu` VALUES (5, 1, 1, '菜单管理', 'Menu', 'system/menu/index', 5, 'menu', 'menu', 0, 0, 0, 'menu:list', NULL, NULL, '2018-12-18 15:17:28', NULL);
INSERT INTO `sys_menu` VALUES (6, NULL, 0, '系统监控', NULL, NULL, 10, 'monitor', 'monitor', 0, 0, 0, NULL, NULL, NULL, '2018-12-18 15:17:48', NULL);
INSERT INTO `sys_menu` VALUES (7, 6, 1, '操作日志', 'Log', 'monitor/log/index', 11, 'log', 'logs', 0, 1, 0, NULL, NULL, 'admin', '2018-12-18 15:18:26', '2020-06-06 13:11:57');
INSERT INTO `sys_menu` VALUES (9, 6, 1, 'SQL监控', 'Sql', 'monitor/sql/index', 18, 'sqlMonitor', 'druid', 0, 0, 0, NULL, NULL, NULL, '2018-12-18 15:19:34', NULL);
INSERT INTO `sys_menu` VALUES (10, NULL, 0, '组件管理', NULL, NULL, 50, 'zujian', 'components', 0, 0, 0, NULL, NULL, NULL, '2018-12-19 13:38:16', NULL);
INSERT INTO `sys_menu` VALUES (11, 10, 1, '图标库', 'Icons', 'components/icons/index', 51, 'icon', 'icon', 0, 0, 0, NULL, NULL, NULL, '2018-12-19 13:38:49', NULL);
INSERT INTO `sys_menu` VALUES (14, 36, 1, '邮件工具', 'Email', 'tools/email/index', 35, 'email', 'email', 0, 0, 0, NULL, NULL, NULL, '2018-12-27 10:13:09', NULL);
INSERT INTO `sys_menu` VALUES (15, 10, 1, '富文本', 'Editor', 'components/Editor', 52, 'fwb', 'tinymce', 0, 0, 0, NULL, NULL, NULL, '2018-12-27 11:58:25', NULL);
INSERT INTO `sys_menu` VALUES (18, 36, 1, '存储管理', 'Storage', 'tools/storage/index', 34, 'qiniu', 'storage', 0, 0, 0, 'storage:list', NULL, NULL, '2018-12-31 11:12:15', NULL);
INSERT INTO `sys_menu` VALUES (19, 36, 1, '支付宝工具', 'AliPay', 'tools/aliPay/index', 37, 'alipay', 'aliPay', 0, 0, 0, NULL, NULL, NULL, '2018-12-31 14:52:38', NULL);
INSERT INTO `sys_menu` VALUES (28, 1, 1, '任务调度', 'Timing', 'system/timing/index', 999, 'timing', 'timing', 0, 0, 0, 'timing:list', NULL, NULL, '2019-01-07 20:34:40', NULL);
INSERT INTO `sys_menu` VALUES (30, 36, 1, '代码生成', 'GeneratorIndex', 'generator/index', 32, 'dev', 'generator', 0, 1, 0, NULL, NULL, NULL, '2019-01-11 15:45:55', NULL);
INSERT INTO `sys_menu` VALUES (32, 6, 1, '异常日志', 'ErrorLog', 'monitor/log/errorLog', 12, 'error', 'errorLog', 0, 0, 0, NULL, NULL, NULL, '2019-01-13 13:49:03', NULL);
INSERT INTO `sys_menu` VALUES (33, 10, 1, 'Markdown', 'Markdown', 'components/MarkDown', 53, 'markdown', 'markdown', 0, 0, 0, NULL, NULL, NULL, '2019-03-08 13:46:44', NULL);
INSERT INTO `sys_menu` VALUES (34, 10, 1, 'Yaml编辑器', 'YamlEdit', 'components/YamlEdit', 54, 'dev', 'yaml', 0, 0, 0, NULL, NULL, NULL, '2019-03-08 15:49:40', NULL);
INSERT INTO `sys_menu` VALUES (35, 1, 1, '部门管理', 'Dept', 'system/dept/index', 6, 'dept', 'dept', 0, 0, 0, 'dept:list', NULL, NULL, '2019-03-25 09:46:00', NULL);
INSERT INTO `sys_menu` VALUES (36, NULL, 0, '系统工具', NULL, '', 30, 'sys-tools', 'sys-tools', 0, 0, 0, NULL, NULL, NULL, '2019-03-29 10:57:35', NULL);
INSERT INTO `sys_menu` VALUES (37, 1, 1, '岗位管理', 'Job', 'system/job/index', 7, 'Steve-Jobs', 'job', 0, 0, 0, 'job:list', NULL, NULL, '2019-03-29 13:51:18', NULL);
INSERT INTO `sys_menu` VALUES (38, 36, 1, '接口文档', 'Swagger', 'tools/swagger/index', 36, 'swagger', 'swagger2', 0, 0, 0, NULL, NULL, NULL, '2019-03-29 19:57:53', NULL);
INSERT INTO `sys_menu` VALUES (39, 1, 1, '字典管理', 'Dict', 'system/dict/index', 8, 'dictionary', 'dict', 0, 0, 0, 'dict:list', NULL, NULL, '2019-04-10 11:49:04', NULL);
INSERT INTO `sys_menu` VALUES (41, 6, 1, '在线用户', 'OnlineUser', 'monitor/online/index', 10, 'Steve-Jobs', 'online', 0, 0, 0, NULL, NULL, NULL, '2019-10-26 22:08:43', NULL);
INSERT INTO `sys_menu` VALUES (44, 2, 2, '用户新增', NULL, '', 2, '', '', 0, 0, 0, 'user:add', NULL, NULL, '2019-10-29 10:59:46', NULL);
INSERT INTO `sys_menu` VALUES (45, 2, 2, '用户编辑', NULL, '', 3, '', '', 0, 0, 0, 'user:edit', NULL, NULL, '2019-10-29 11:00:08', NULL);
INSERT INTO `sys_menu` VALUES (46, 2, 2, '用户删除', NULL, '', 4, '', '', 0, 0, 0, 'user:del', NULL, NULL, '2019-10-29 11:00:23', NULL);
INSERT INTO `sys_menu` VALUES (48, 3, 2, '角色创建', NULL, '', 2, '', '', 0, 0, 0, 'roles:add', NULL, NULL, '2019-10-29 12:45:34', NULL);
INSERT INTO `sys_menu` VALUES (49, 3, 2, '角色修改', NULL, '', 3, '', '', 0, 0, 0, 'roles:edit', NULL, NULL, '2019-10-29 12:46:16', NULL);
INSERT INTO `sys_menu` VALUES (50, 3, 2, '角色删除', NULL, '', 4, '', '', 0, 0, 0, 'roles:del', NULL, NULL, '2019-10-29 12:46:51', NULL);
INSERT INTO `sys_menu` VALUES (52, 5, 2, '菜单新增', NULL, '', 2, '', '', 0, 0, 0, 'menu:add', NULL, NULL, '2019-10-29 12:55:07', NULL);
INSERT INTO `sys_menu` VALUES (53, 5, 2, '菜单编辑', NULL, '', 3, '', '', 0, 0, 0, 'menu:edit', NULL, NULL, '2019-10-29 12:55:40', NULL);
INSERT INTO `sys_menu` VALUES (54, 5, 2, '菜单删除', NULL, '', 4, '', '', 0, 0, 0, 'menu:del', NULL, NULL, '2019-10-29 12:56:00', NULL);
INSERT INTO `sys_menu` VALUES (56, 35,2, '部门新增', NULL, '', 2, '', '', 0, 0, 0, 'dept:add', NULL, NULL, '2019-10-29 12:57:09', NULL);
INSERT INTO `sys_menu` VALUES (57, 35, 2, '部门编辑', NULL, '', 3, '', '', 0, 0, 0, 'dept:edit', NULL, NULL, '2019-10-29 12:57:27', NULL);
INSERT INTO `sys_menu` VALUES (58, 35, 2, '部门删除', NULL, '', 4, '', '', 0, 0, 0, 'dept:del', NULL, NULL, '2019-10-29 12:57:41', NULL);
INSERT INTO `sys_menu` VALUES (60, 37, 2, '岗位新增', NULL, '', 2, '', '', 0, 0, 0, 'job:add', NULL, NULL, '2019-10-29 12:58:27', NULL);
INSERT INTO `sys_menu` VALUES (61, 37, 2, '岗位编辑', NULL, '', 3, '', '', 0, 0, 0, 'job:edit', NULL, NULL, '2019-10-29 12:58:45', NULL);
INSERT INTO `sys_menu` VALUES (62, 37, 2, '岗位删除', NULL, '', 4, '', '', 0, 0, 0, 'job:del', NULL, NULL, '2019-10-29 12:59:04', NULL);
INSERT INTO `sys_menu` VALUES (64, 39, 2, '字典新增', NULL, '', 2, '', '', 0, 0, 0, 'dict:add', NULL, NULL, '2019-10-29 13:00:17', NULL);
INSERT INTO `sys_menu` VALUES (65, 39, 2, '字典编辑', NULL, '', 3, '', '', 0, 0, 0, 'dict:edit', NULL, NULL, '2019-10-29 13:00:42', NULL);
INSERT INTO `sys_menu` VALUES (66, 39, 2, '字典删除', NULL, '', 4, '', '', 0, 0, 0, 'dict:del', NULL, NULL, '2019-10-29 13:00:59', NULL);
INSERT INTO `sys_menu` VALUES (73, 28, 2, '任务新增', NULL, '', 2, '', '', 0, 0, 0, 'timing:add', NULL, NULL, '2019-10-29 13:07:28', NULL);
INSERT INTO `sys_menu` VALUES (74, 28, 2, '任务编辑', NULL, '', 3, '', '', 0, 0, 0, 'timing:edit', NULL, NULL, '2019-10-29 13:07:41', NULL);
INSERT INTO `sys_menu` VALUES (75, 28, 2, '任务删除', NULL, '', 4, '', '', 0, 0, 0, 'timing:del', NULL, NULL, '2019-10-29 13:07:54', NULL);
INSERT INTO `sys_menu` VALUES (77, 18, 2, '上传文件', NULL, '', 2, '', '', 0, 0, 0, 'storage:add', NULL, NULL, '2019-10-29 13:09:09', NULL);
INSERT INTO `sys_menu` VALUES (78, 18, 2, '文件编辑', NULL, '', 3, '', '', 0, 0, 0, 'storage:edit', NULL, NULL, '2019-10-29 13:09:22', NULL);
INSERT INTO `sys_menu` VALUES (79, 18, 2, '文件删除', NULL, '', 4, '', '', 0, 0, 0, 'storage:del', NULL, NULL, '2019-10-29 13:09:34', NULL);
INSERT INTO `sys_menu` VALUES (80, 6, 1, '服务监控', 'ServerMonitor', 'monitor/server/index', 14, 'codeConsole', 'server', 0, 0, 0, 'monitor:list', NULL, 'admin', '2019-11-07 13:06:39', '2020-05-04 18:20:50');
INSERT INTO `sys_menu` VALUES (82, 36, 1, '生成配置', 'GeneratorConfig', 'generator/config', 33, 'dev', 'generator/config/:tableName', 0, 1, 1, NULL, NULL, NULL, '2019-11-17 20:08:56', NULL);
INSERT INTO `sys_menu` VALUES (83, 10, 1, '图表库', 'Echarts', 'components/Echarts', 50, 'chart', 'echarts', 0, 1, 0, NULL, NULL, NULL, '2019-11-21 09:04:32', NULL);
INSERT INTO `sys_menu` VALUES (90, NULL, 1, '运维管理', 'Mnt', '', 20, 'mnt', 'mnt', 0, 0, 0, NULL, NULL, NULL, '2019-11-09 10:31:08', NULL);
INSERT INTO `sys_menu` VALUES (92, 90, 1, '服务器', 'ServerDeploy', 'mnt/server/index', 22, 'server', 'mnt/serverDeploy', 0, 0, 0, 'serverDeploy:list', NULL, NULL, '2019-11-10 10:29:25', NULL);
INSERT INTO `sys_menu` VALUES (93, 90, 1, '应用管理', 'App', 'mnt/app/index', 23, 'app', 'mnt/app', 0, 0, 0, 'app:list', NULL, NULL, '2019-11-10 11:05:16', NULL);
INSERT INTO `sys_menu` VALUES (94, 90, 1, '部署管理', 'Deploy', 'mnt/deploy/index', 24, 'deploy', 'mnt/deploy', 0, 0, 0, 'deploy:list', NULL, NULL, '2019-11-10 15:56:55', NULL);
INSERT INTO `sys_menu` VALUES (97, 90, 1, '部署备份', 'DeployHistory', 'mnt/deployHistory/index', 25, 'backup', 'mnt/deployHistory', 0, 0, 0, 'deployHistory:list', NULL, NULL, '2019-11-10 16:49:44', NULL);
INSERT INTO `sys_menu` VALUES (98, 90, 1, '数据库管理', 'Database', 'mnt/database/index', 26, 'database', 'mnt/database', 0, 0, 0, 'database:list', NULL, NULL, '2019-11-10 20:40:04', NULL);
INSERT INTO `sys_menu` VALUES (102, 97, 2, '删除', NULL, '', 999, '', '', 0, 0, 0, 'deployHistory:del', NULL, NULL, '2019-11-17 09:32:48', NULL);
INSERT INTO `sys_menu` VALUES (103, 92, 2, '服务器新增', NULL, '', 999, '', '', 0, 0, 0, 'serverDeploy:add', NULL, NULL, '2019-11-17 11:08:33', NULL);
INSERT INTO `sys_menu` VALUES (104, 92, 2, '服务器编辑', NULL, '', 999, '', '', 0, 0, 0, 'serverDeploy:edit', NULL, NULL, '2019-11-17 11:08:57', NULL);
INSERT INTO `sys_menu` VALUES (105, 92, 2, '服务器删除', NULL, '', 999, '', '', 0, 0, 0, 'serverDeploy:del', NULL, NULL, '2019-11-17 11:09:15', NULL);
INSERT INTO `sys_menu` VALUES (106, 93, 2, '应用新增', NULL, '', 999, '', '', 0, 0, 0, 'app:add', NULL, NULL, '2019-11-17 11:10:03', NULL);
INSERT INTO `sys_menu` VALUES (107, 93, 2, '应用编辑', NULL, '', 999, '', '', 0, 0, 0, 'app:edit', NULL, NULL, '2019-11-17 11:10:28', NULL);
INSERT INTO `sys_menu` VALUES (108, 93, 2, '应用删除', NULL, '', 999, '', '', 0, 0, 0, 'app:del', NULL, NULL, '2019-11-17 11:10:55', NULL);
INSERT INTO `sys_menu` VALUES (109, 94, 2, '部署新增', NULL, '', 999, '', '', 0, 0, 0, 'deploy:add', NULL, NULL, '2019-11-17 11:11:22', NULL);
INSERT INTO `sys_menu` VALUES (110, 94, 2, '部署编辑', NULL, '', 999, '', '', 0, 0, 0, 'deploy:edit', NULL, NULL, '2019-11-17 11:11:41', NULL);
INSERT INTO `sys_menu` VALUES (111, 94, 2, '部署删除', NULL, '', 999, '', '', 0, 0, 0, 'deploy:del', NULL, NULL, '2019-11-17 11:12:01', NULL);
INSERT INTO `sys_menu` VALUES (112, 98, 2, '数据库新增', NULL, '', 999, '', '', 0, 0, 0, 'database:add', NULL, NULL, '2019-11-17 11:12:43', NULL);
INSERT INTO `sys_menu` VALUES (113, 98, 2, '数据库编辑', NULL, '', 999, '', '', 0, 0, 0, 'database:edit', NULL, NULL, '2019-11-17 11:12:58', NULL);
INSERT INTO `sys_menu` VALUES (114, 98, 2, '数据库删除', NULL, '', 999, '', '', 0, 0, 0, 'database:del', NULL, NULL, '2019-11-17 11:13:14', NULL);
INSERT INTO `sys_menu` VALUES (116, 36, 1, '生成预览', 'Preview', 'generator/preview', 999, 'java', 'generator/preview/:tableName', 0, 1, 1, NULL, NULL, NULL, '2019-11-26 14:54:36', NULL);
COMMIT;
sys_role
角色表
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名称',
`level` int(2) DEFAULT NULL COMMENT '角色级别',
`description` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '描述',
`data_scope` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '数据权限',
`create_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '创建操作人',
`update_by` varchar(50) CHARACTER SET utf8 DEFAULT NULL COMMENT '更新操作人',
`create_time` datetime DEFAULT NULL COMMENT '创建日期',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='角色表';
-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` VALUES (1, '超级管理员', 1, '-', '全部', 'admin', 'admin', '2018-11-23 11:04:37', '2020-08-06 16:10:24');
INSERT INTO `sys_role` VALUES (2, '普通用户', 2, '-', '本级', 'admin', 'admin', '2018-11-23 13:09:06', '2020-09-05 10:45:12');
COMMIT;
sys_roles_menus
角色菜单中间表
-- ----------------------------
-- Table structure for sys_roles_menus
-- ----------------------------
DROP TABLE IF EXISTS `sys_roles_menus`;
CREATE TABLE `sys_roles_menus` (
`menu_id` int(11) NOT NULL COMMENT '菜单ID',
`role_id` int(11) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`menu_id`,`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='角色菜单关联';
-- ----------------------------
-- Records of sys_roles_menus
-- ----------------------------
BEGIN;
INSERT INTO `sys_roles_menus` VALUES (1, 1);
INSERT INTO `sys_roles_menus` VALUES (1, 2);
INSERT INTO `sys_roles_menus` VALUES (2, 1);
INSERT INTO `sys_roles_menus` VALUES (2, 2);
INSERT INTO `sys_roles_menus` VALUES (3, 1);
INSERT INTO `sys_roles_menus` VALUES (5, 1);
INSERT INTO `sys_roles_menus` VALUES (6, 1);
INSERT INTO `sys_roles_menus` VALUES (6, 2);
INSERT INTO `sys_roles_menus` VALUES (7, 1);
INSERT INTO `sys_roles_menus` VALUES (7, 2);
INSERT INTO `sys_roles_menus` VALUES (9, 1);
INSERT INTO `sys_roles_menus` VALUES (9, 2);
INSERT INTO `sys_roles_menus` VALUES (10, 1);
INSERT INTO `sys_roles_menus` VALUES (10, 2);
INSERT INTO `sys_roles_menus` VALUES (11, 1);
INSERT INTO `sys_roles_menus` VALUES (11, 2);
INSERT INTO `sys_roles_menus` VALUES (14, 1);
INSERT INTO `sys_roles_menus` VALUES (14, 2);
INSERT INTO `sys_roles_menus` VALUES (15, 1);
INSERT INTO `sys_roles_menus` VALUES (15, 2);
INSERT INTO `sys_roles_menus` VALUES (18, 1);
INSERT INTO `sys_roles_menus` VALUES (19, 1);
INSERT INTO `sys_roles_menus` VALUES (19, 2);
INSERT INTO `sys_roles_menus` VALUES (21, 1);
INSERT INTO `sys_roles_menus` VALUES (21, 2);
INSERT INTO `sys_roles_menus` VALUES (22, 1);
INSERT INTO `sys_roles_menus` VALUES (22, 2);
INSERT INTO `sys_roles_menus` VALUES (23, 1);
INSERT INTO `sys_roles_menus` VALUES (23, 2);
INSERT INTO `sys_roles_menus` VALUES (24, 1);
INSERT INTO `sys_roles_menus` VALUES (24, 2);
INSERT INTO `sys_roles_menus` VALUES (27, 1);
INSERT INTO `sys_roles_menus` VALUES (27, 2);
INSERT INTO `sys_roles_menus` VALUES (28, 1);
INSERT INTO `sys_roles_menus` VALUES (30, 1);
INSERT INTO `sys_roles_menus` VALUES (30, 2);
INSERT INTO `sys_roles_menus` VALUES (32, 1);
INSERT INTO `sys_roles_menus` VALUES (32, 2);
INSERT INTO `sys_roles_menus` VALUES (33, 1);
INSERT INTO `sys_roles_menus` VALUES (33, 2);
INSERT INTO `sys_roles_menus` VALUES (34, 1);
INSERT INTO `sys_roles_menus` VALUES (34, 2);
INSERT INTO `sys_roles_menus` VALUES (35, 1);
INSERT INTO `sys_roles_menus` VALUES (36, 1);
INSERT INTO `sys_roles_menus` VALUES (36, 2);
INSERT INTO `sys_roles_menus` VALUES (37, 1);
INSERT INTO `sys_roles_menus` VALUES (38, 1);
INSERT INTO `sys_roles_menus` VALUES (39, 1);
INSERT INTO `sys_roles_menus` VALUES (41, 1);
INSERT INTO `sys_roles_menus` VALUES (44, 1);
INSERT INTO `sys_roles_menus` VALUES (45, 1);
INSERT INTO `sys_roles_menus` VALUES (46, 1);
INSERT INTO `sys_roles_menus` VALUES (48, 1);
INSERT INTO `sys_roles_menus` VALUES (49, 1);
INSERT INTO `sys_roles_menus` VALUES (50, 1);
INSERT INTO `sys_roles_menus` VALUES (52, 1);
INSERT INTO `sys_roles_menus` VALUES (53, 1);
INSERT INTO `sys_roles_menus` VALUES (54, 1);
INSERT INTO `sys_roles_menus` VALUES (56, 1);
INSERT INTO `sys_roles_menus` VALUES (57, 1);
INSERT INTO `sys_roles_menus` VALUES (58, 1);
INSERT INTO `sys_roles_menus` VALUES (60, 1);
INSERT INTO `sys_roles_menus` VALUES (61, 1);
INSERT INTO `sys_roles_menus` VALUES (62, 1);
INSERT INTO `sys_roles_menus` VALUES (64, 1);
INSERT INTO `sys_roles_menus` VALUES (65, 1);
INSERT INTO `sys_roles_menus` VALUES (66, 1);
INSERT INTO `sys_roles_menus` VALUES (73, 1);
INSERT INTO `sys_roles_menus` VALUES (74, 1);
INSERT INTO `sys_roles_menus` VALUES (75, 1);
INSERT INTO `sys_roles_menus` VALUES (77, 1);
INSERT INTO `sys_roles_menus` VALUES (78, 1);
INSERT INTO `sys_roles_menus` VALUES (79, 1);
INSERT INTO `sys_roles_menus` VALUES (80, 1);
INSERT INTO `sys_roles_menus` VALUES (80, 2);
INSERT INTO `sys_roles_menus` VALUES (82, 1);
INSERT INTO `sys_roles_menus` VALUES (82, 2);
INSERT INTO `sys_roles_menus` VALUES (83, 1);
INSERT INTO `sys_roles_menus` VALUES (83, 2);
INSERT INTO `sys_roles_menus` VALUES (90, 1);
INSERT INTO `sys_roles_menus` VALUES (92, 1);
INSERT INTO `sys_roles_menus` VALUES (93, 1);
INSERT INTO `sys_roles_menus` VALUES (94, 1);
INSERT INTO `sys_roles_menus` VALUES (97, 1);
INSERT INTO `sys_roles_menus` VALUES (98, 1);
INSERT INTO `sys_roles_menus` VALUES (102, 1);
INSERT INTO `sys_roles_menus` VALUES (103, 1);
INSERT INTO `sys_roles_menus` VALUES (104, 1);
INSERT INTO `sys_roles_menus` VALUES (105, 1);
INSERT INTO `sys_roles_menus` VALUES (106, 1);
INSERT INTO `sys_roles_menus` VALUES (107, 1);
INSERT INTO `sys_roles_menus` VALUES (108, 1);
INSERT INTO `sys_roles_menus` VALUES (109, 1);
INSERT INTO `sys_roles_menus` VALUES (110, 1);
INSERT INTO `sys_roles_menus` VALUES (111, 1);
INSERT INTO `sys_roles_menus` VALUES (112, 1);
INSERT INTO `sys_roles_menus` VALUES (113, 1);
INSERT INTO `sys_roles_menus` VALUES (114, 1);
INSERT INTO `sys_roles_menus` VALUES (116, 1);
INSERT INTO `sys_roles_menus` VALUES (116, 2);
INSERT INTO `sys_roles_menus` VALUES (120, 1);
COMMIT;
sys_users_roles
用户角色中间表
-- ----------------------------
-- Table structure for sys_users_roles
-- ----------------------------
DROP TABLE IF EXISTS `sys_users_roles`;
CREATE TABLE `sys_users_roles` (
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`role_id` int(11) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=COMPACT COMMENT='用户角色关联';
-- ----------------------------
-- Records of sys_users_roles
-- ----------------------------
BEGIN;
INSERT INTO `sys_users_roles` VALUES (1, 1);
INSERT INTO `sys_users_roles` VALUES (2, 2);
COMMIT;
- 在
loadUserByUsername
中增加权限读取
package com.cz.config.security;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cz.entity.SysMenu;
import com.cz.entity.SysUser;
import com.cz.service.ISysMenuService;
import com.cz.service.ISysUserService;
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.stereotype.Service;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private ISysUserService sysUserService;
@Autowired
private ISysMenuService sysMenuService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//通过用户名查询用户信息
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, userName));
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户不存在");
}
List<GrantedAuthority> grantedAuthorities = getGrantedAuthorities(user);//获取用户权限
return new User(user.getUsername(), user.getPassword(), user.getEnabled(), true, true, true, grantedAuthorities);
}
/**
* 后续优化会放入缓存,这里先每次查库
*
* @param user
* @return
*/
public List<GrantedAuthority> getGrantedAuthorities(SysUser user) {
List<String> permissions;
//是否超管
if (user.getIsAdmin()) {
//超管直接查所有权限
LambdaQueryWrapper<SysMenu> queryWrapper = new LambdaQueryWrapper<SysMenu>().select(SysMenu::getPermission).isNotNull(SysMenu::getPermission);
permissions = sysMenuService.listObjs(queryWrapper, Object::toString);
//只有超管支持角色访问,因为有些接口只允许超管请求,其他绝对严格通过权限标识符控制
permissions.add("ROLE_admin");
} else {
//其他角色查询权限
permissions = sysMenuService.findPermissionByUserId(user.getUserId());
}
String collect = String.join(",", permissions);
return AuthorityUtils.commaSeparatedStringToAuthorityList(collect);
}
}
ISysMenuService
的findPermissionByUserId
→ 其他角色查询权限
@Service
public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {
@Autowired
private SysMenuMapper sysMenuMapper;
@Override
public List<String> findPermissionByUserId(Long userId) {
return sysMenuMapper.findPermissionByUserId(userId);
}
}
SysMenuMapper
的findPermissionByUserId
<select id="findPermissionByUserId" resultType="java.lang.String">
select sm.permission permission from sys_menu sm INNER JOIN sys_roles_menus srm on sm.menu_id = srm.menu_id and sm.permission is not null INNER JOIN sys_users_roles srr on srr.role_id = srm.role_id
where srr.user_id=#{userId}
</select>
2.4 测试
- 编写
controller
@PreAuthorize("hasAnyRole('admin')")
@GetMapping("/user")
public ResultVO info(Principal principal){
return ResponseUtil.success(principal) ;
}
admin
登录,访问/user
返回成功,test
登录,访问/user
返回403,forbidden
,测试完毕
hasAnyRole('admin')
:管理员才能访问
hasAuthority('user:add')
:需要有该权限的人才能访问
什么也不加:登录就能访问
不登录就能访问的需要在配置文件里做处理:.antMatchers("/r/r2").permitALL()
3. 自定义权限不足页面
-
默认权限不足是一个很不友好的403 forbidden ,改造成自己的页面
-
前端:
resources/static/noAuth.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
权限不足!
</body>
</html>
- 配置
SecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.accessDeniedPage("/noAuth.html") //权限不足跳转到这个页面
//...省略
}
- 启动项目,测试无误
4. 基于注解的方法级别权限控制
可以替代SecurityConfig配置类里的.antMatchers("/r/r1").hasAnyRole("p1")
的配置
@Secured
用户具有某个权限,才能访问此方法
-
开启全局配置注解
@EnableGlobalMethodSecurity(securedEnabled = true)
,加到自己的SecurityConfig配置类或者程序启动类都可以 -
添加注解到方法上
@GetMapping("/r/r1")
@Secured({"ROLE_p1"})
public String r1(){
return "访问r1资源";
}
@PreAuthorize
进入方法前拦截,校验权限
-
开启全局配置注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
-
添加注解到方法上
@GetMapping("/r/r1")
@PreAuthorize("hasAnyAuthority('p1')")
public String r1(){
return "访问r1资源";
}
@PostAuthorize
方法执行后拦截,校验权限
-
开启全局配置注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
-
添加注解到方法上
@GetMapping("/r/r1")
@PreAuthorize("hasAnyAuthority('p1')")
public String r1(){
System.out.println("方法执行");
return "访问r1资源";
}
这个注解的特点是,会先执行后校验,类似 do.....while 循环一样
这里需要注意一个点,基于注解的权限控制,当权限不足时,会抛出全局AccessDeniedException异常,无法被我们上面写的handler所处理 ,他是路由层面的,只有通过配置文件的.antMatchers("/r/r1").hasAnyAuthority("p1")
才会被处理,所以需要增加全局异常处理
@ResponseBody
@ControllerAdvice
@Slf4j
public class ExceptionHandlers {
/**
* 无权访问异常处理器
*
* @param e security无权访问处理器
* @return ResponseResult
*/
@ExceptionHandler(AccessDeniedException.class)
public ResultVO parameterMissingExceptionHandler(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) {
log.error("【无权访问异常】: err = {} url = {} ", e.getMessage(), request.getRequestURL());
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return ResponseUtil.error(ResultEnum.NO_AUTH_ACCESS);
}
}
5. 基于注解的数据级别控制
@PostFilter
对返回数据进行过滤控制
-
开启全局配置注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
-
添加注解到方法上
@GetMapping("/r/r1")
@PostFilter("filterObject.username=='admin1'")
public List<User> r1(){
List<User> list = Lists.newArrayList();
list.add(new User(1L,"admin1","123"));
list.add(new User(2L,"admin1123","1231"));
System.out.println(list);
return list;
}
启动服务测试,请求/r/r1
,返回结果:
@PreFilter
对传入参数进行过滤控制
-
开启全局配置注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
-
添加注解到方法上
UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public List<Integer> delete(List<Integer> ids) {
System.out.println(ids); //[2,4]
return ids;
}
}
- 调用
@Autowired
private IUserService userService;
@GetMapping("/r/r1")
public List<Integer> r1(Integer id){
List<Integer> ids = new ArrayList<Integer>();
ids.add(1);
ids.add(2);
ids.add(3);
ids.add(4);
ids.add(5);
userService.delete(ids);
return ids;//[2,4]
}
- 启动,测试,返回结果 2,4,打印的ids 也是2,4 ,说明生效
- 三个注解一起使用下,调用还是上面的
/r/r1
,传入1,2,3,4,5的list
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@PreFilter(filterTarget="ids", value="filterObject%2==0") //
@PostFilter("filterObject==2")
@PreAuthorize("hasAnyAuthority('p1')")
public List<Integer> delete(List<Integer> ids) {
System.out.println(ids);[2,4]
return ids;
}
}
启动测试,访问/r/r1
,被拦截到登录,说明权限校验生效,返回[2]
,说明@PreFilter
和@PostFilter
都生效了。over!
6. 自定义用户注销页
-
默认退出路径
/logout
,退出登录后默认重定向到login页面 -
修改配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.accessDeniedPage("/noAuth.html") //自定义权限不足页
.and()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyRole("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.anyRequest().permitAll() //其他请求全部放行
.and()
.formLogin()
.loginPage("/login.html") //自定义登录页
.loginProcessingUrl("/loginAction") //自定义登录接口
.and()
.logout()
.logoutSuccessUrl("/loginOut") //自定义退出登录成功接口
.logoutUrl("/lg"); //自定义退出登录接口
}
- 增加退出登录成功处理接口
@GetMapping("/loginOut")
public ResultVO login(){
return "退出成功";
}
启动测试,访问/r/r1
,被拦截到登录,进行登录,然后访问 /lg
, 返回 退出成功
,测试完毕
7. 记住我
- 记住我需要保存登录
token
,我们这里用数据库存储,创建表,建表语句在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)
-
在
SecurityConfig
里增加配置 -
配置数据源
@Resource
private DataSource dataSource;
- 配置
remember-me
存储器
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//如果不使用上面的建表语句进行建表,也可以通过这个方式自动创建表,但是如果已经有表了,请把它删掉,否则还会去尝试创建,然后就报错
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
- 配置
HttpScurity
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.accessDeniedPage("/noAuth.html")
.and()
.authorizeRequests()
.antMatchers("/r/r1").hasAnyRole("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/loginAction")
.and()
.logout()
.logoutSuccessUrl("/loginOut")
.logoutUrl("/lg")
.and()
.rememberMe()//开启功能
.tokenRepository(persistentTokenRepository()) //设置token存储
.tokenValiditySeconds(60); //设置过期时间,单位秒
}
- 在login.html里增加复选框
<form action="/api/loginAction" method="post">
<div>
用户名:<input type="text" name="username"/>
</div>
<div>
密码:<input type="password" name="password"/>
</div>
<div>
记住我:<input type="checkbox" name="remember-me"/> <!--默认name就是remember-me,也可以通过修改配置字段名-->
</div>
<div>
<input type="submit" value="登录"/>
</div>
</form>
启动测试,登录时勾选复选框
登录成功后,F12查看cookie
再看数据库
ok,功能正常。
高级
1. 前后端分离
-
前后端分离,所有请求均以json返回,不再返回页面。
-
示例:以下代码就可以直接将各种情况以JSON的方式返回
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.permitAll()
//登录失败,返回json
.failureHandler((request,response,ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",401);
if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
map.put("message","用户名或密码错误");
} else if (ex instanceof DisabledException) {
map.put("message","账户被禁用");
} else {
map.put("message","登录失败!");
}
out.write(map.toString());
out.flush();
out.close();
})
//登录成功,返回json
.successHandler((request,response,authentication) -> {
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",200);
map.put("message","登录成功");
map.put("data",authentication);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(map.toString());
out.flush();
out.close();
})
.and()
.exceptionHandling()
//没有权限,返回json
.accessDeniedHandler((request,response,ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
PrintWriter out = response.getWriter();
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",403);
map.put("message", "权限不足");
out.write(map.toString());
out.flush();
out.close();
})
//未登录或登录失效时,返回json
.authenticationEntryPoint((request,response,ex) -> {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",401);
map.put("message","未登录");
out.write(map.toString());
out.flush();
out.close();
})
.and()
.logout()
//退出成功,返回json
.logoutSuccessHandler((request,response,authentication) -> {
Map<String,Object> map = new HashMap<String,Object>();
map.put("code",200);
map.put("message","退出成功");
map.put("data",authentication);
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(map.toString());
out.flush();
out.close();
});
}
2. 代码封装与统一化处理
2.1登录失败
- 自定义的登录失败错误处理
handler
/**
* 登录失败处理handler
*/
@Component
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
if (e instanceof CredentialsExpiredException) { //密码已过期
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.PASSWORD_IS_EXPIRED);
} else if (e instanceof AccountExpiredException) {//账号已过期
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_EXPIRED);
} else if (e instanceof DisabledException) {//账号已禁用
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_DISABLED);
} else {//用户名或密码错误
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.USERNAME_OR_PASSWORD_ERROR);
}
}
}
- 添加
handler
到Security
的配置中
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.accessDeniedPage("/noAuth")
.and()
.authorizeRequests()
.antMatchers("/r/r2").hasAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successForwardUrl("/login/success")
.failureHandler(customAuthFailureHandler) //添加错误处理handler
.and()
.logout()
.logoutSuccessUrl("/login/out");
}
启动测试,输入错误用户名密码,可返回JSON
-
ResponseUtil
与ResultEnum
代码 -
ResponseUtil
package com.cz.utils;
import com.cz.enums.ResultEnum;
import com.cz.model.vo.ResultVO;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Auther: chaixuhong
*/
public class ResponseUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
/**
* 错误返回
*
* @param resultEnum
* @return
*/
public static ResultVO error(ResultEnum resultEnum) {
ResultVO resultVO = new ResultVO();
resultVO.setCode(resultEnum.getCode());
resultVO.setErrMsg(resultEnum.getErrMsg());
return resultVO;
}
/**
* 自定义返回
*
* @param code
* @param errMsg
* @return
*/
public static ResultVO error(int code, String errMsg) {
ResultVO resultVO = new ResultVO();
resultVO.setCode(code);
resultVO.setErrMsg(errMsg);
return resultVO;
}
/**
* 有参成功返回
*
* @param data
* @return
*/
public static ResultVO success(Object data) {
ResultVO resultVO = new ResultVO();
resultVO.setCode(ResultEnum.SUCCESS.getCode());
resultVO.setErrMsg(ResultEnum.SUCCESS.getErrMsg());
resultVO.setData(data);
return resultVO;
}
/**
* 无参成功返回
*
* @return
*/
public static ResultVO success() {
ResultVO resultVO = new ResultVO();
resultVO.setCode(ResultEnum.SUCCESS.getCode());
resultVO.setErrMsg(ResultEnum.SUCCESS.getErrMsg());
return resultVO;
}
/**
* 标准枚举输出
*
* @param response
* @param httpStatus
* @param resultEnum
* @throws IOException
*/
public static void printOut(HttpServletResponse response, int httpStatus, ResultEnum resultEnum) throws IOException {
ResultVO resultVO = new ResultVO();
resultVO.setCode(resultEnum.getCode());
resultVO.setErrMsg(resultEnum.getErrMsg());
response.setContentType("application/json;charset=utf-8");
response.setStatus(httpStatus);
PrintWriter out = response.getWriter();
out.write(objectMapper.writeValueAsString(resultVO));
out.flush();
out.close();
}
}
ResultEnum
package com.cz.enums;
import lombok.Getter;
@Getter
public enum ResultEnum {
SUCCESS(1, "success"),
SERVER_ERROR(500,"服务器异常!"),
/**
* 登录异常码
*/
USERNAME_OR_PASSWORD_ERROR(800,"用户名或密码错误"),
ACCOUNT_IS_LOCKED(801,"账号被锁定,请联系管理员"),
PASSWORD_IS_EXPIRED(802,"密码已过期,请联系管理员"),
ACCOUNT_IS_EXPIRED(803,"账号已过期,请联系管理员"),
ACCOUNT_IS_DISABLED(804,"账号被禁用,请联系管理员"),
CODE_ERROR_INPUT(805,"验证码输入错误"),
CODE_IS_EXPIRED(806,"验证码已过期"),
PARAM_ERROR(1001, "参数错误"),
FILE_PARAM_ERROR(1002, "文件不能超过100M"),
;
private int code;
private String errMsg;
ResultEnum(int code, String errMsg) {
this.code = code;
this.errMsg = errMsg;
}
}
2.2登录成功
- 自定义的登录成功处理
handler
@Component
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ResponseUtil.printOutData(httpServletResponse,HttpServletResponse.SC_OK, ResultEnum.SUCCESS,authentication.getPrincipal());
}
}
- 添加
handler
到Security
的配置中
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.accessDeniedPage("/noAuth")
.and()
.authorizeRequests()
.antMatchers("/r/r2").hasAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(customAuthSuccessHandler)//添加成功处理handler
.failureHandler(customAuthFailureHandler)//添加错误处理handler
.and()
.logout()
.logoutSuccessUrl("/login/out");
}
2.3登录失效或未登录
- 自定义的登录失效或未登录处理
handler
,这个很关键,不重写当登录失效或者没有登录请求了需要认证的接口,会重定向到Security
的登录页。重写返回json
@Component
public class CustomAuthExpiredHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.LOGIN_IS_EXPIRED);
}
}
- 添加
handler
到Security
的配置中
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.accessDeniedPage("/noAuth")
.and()
.authorizeRequests()
.antMatchers("/login/fail").hasAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(customAuthSuccessHandler)
.failureHandler(customAuthFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(customAuthExpiredHandler) //设置未登录或登录失效handler
.and()
.logout()
.logoutSuccessUrl("/login/out");
}
2.4无权访问
- 自定义的无权访问处理
handler
/**
* 权限不足
*/
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_UNAUTHORIZED, ResultEnum.NO_AUTH_ACCESS);
}
}
- 添加
handler
到Security
的配置中
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login/fail").hasAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(customAuthSuccessHandler)
.failureHandler(customAuthFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(customAuthExpiredHandler)
.accessDeniedHandler(customAccessDeniedHandler) //权限不足处理器
.and()
.logout()
.logoutSuccessUrl("/login/out");
}
2.5退出登录
- 自定义的退出登录处理
handler
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ResponseUtil.printOut(httpServletResponse,HttpServletResponse.SC_OK, ResultEnum.SUCCESS);
}
}
- 添加
handler
到Security
的配置中
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login/fail").hasAuthority("p2")
.anyRequest().permitAll()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(customAuthSuccessHandler)
.failureHandler(customAuthFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(customAuthExpiredHandler)
.accessDeniedHandler(customAccessDeniedHandler)
.and()
.logout()
.logoutSuccessHandler(customLogoutSuccessHandler);//退出登录处理器
}
3. 验证码登录
- 为防止登录接口被恶意请求,增加验证码登录是必要的,但是其实也是防君子防不了小人
3.1增加获取验证码接口
- 验证码工具
<!-- Java图形验证码 -->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
- 编写验证码接口,生成验证码返回给前端,并且缓存到Redis中
@GetMapping(value = "/code")
@ApiOperation("获取验证码")
public ResponseEntity<Object> getCode(@RequestParam String code) {
ArithmeticCaptcha captcha = new ArithmeticCaptcha(111, 36);
captcha.getArithmeticString(); // 获取运算的公式:4-9+1=?
String text = captcha.text();// 获取运算的结果:-4
System.out.println("获取运算的结果"+text);
String uuid = CodeGeneratedUtil.genUUID();//验证码唯一标识,作为redis的key
// 保存redis
redisUtils.set(uuid, text, 5L, TimeUnit.MINUTES);
// 验证码信息
Map<String, Object> imgResult = new HashMap<String, Object>(2) {{
put("img", captcha.toBase64());
put("uuid", uuid);
}};
return ResponseEntity.ok(imgResult);
}
3.2创建验证码过滤器
- 前面提到过,Security就是一串过滤器,所以我们需要增加自己的过滤器就好了。
验证码过滤器CodeAuthenticationFilter
package com.cz.config.security;
import com.cz.enums.ResultEnum;
import com.cz.exception.ValidateCodeException;
import com.cz.utils.RedisUtil;
import com.cz.utils.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
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.Objects;
/**
* @author chaixuhong
* @className CodeAuthenticationFilter
* @description 验证码登录验证过滤器
* @date 2021年05月12日
**/
@Slf4j
@Component
public class CodeAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private CustomAuthFailureHandler customAuthFailureHandler;//错误处理handler
@Autowired
private RedisUtil redisUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
//TODO:这里url最好后续改成读取配置
if (StringUtils.equals("/api/login", httpServletRequest.getRequestURI())
&& StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {
try {
validateCode(httpServletRequest);
}catch (ValidateCodeException e) {
customAuthFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
/**
* 校验验证码
* @param request
*/
private void validateCode(HttpServletRequest request){
String code = request.getParameter("code");
String uuid = request.getParameter("uuid");
if (StringUtils.isBlank(code)) {
log.error("提交空验证码!");
throw new ValidateCodeException(ResultEnum.CODE_ERROR_INPUT);
}
if (StringUtils.isBlank(uuid)) {
log.error("提交空的uuid!");
throw new ValidateCodeException(ResultEnum.CODE_ERROR_INPUT);
}
String redisCode = redisUtil.get(uuid);
if (redisCode == null) {
throw new ValidateCodeException(ResultEnum.CODE_IS_EXPIRED);
}
if (!StringUtils.equals(redisCode, code)) {
throw new ValidateCodeException(ResultEnum.CODE_ERROR_INPUT);
}
redisUtil.delete(uuid);
}
}
3.3 在登录失败handler中处理自定义异常
- 自定义的登录失败错误处理handler。新增关于
ValidateCodeException
异常的处理
/**
* 登录失败处理handler
*/
@Component
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
if (e instanceof ValidateCodeException) {//验证码错误或失效
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ((ValidateCodeException) e).getResultEnum());
} else if (e instanceof LockedException) { //账号被锁定
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_LOCKED);
} else if (e instanceof CredentialsExpiredException) { //密码已过期
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.PASSWORD_IS_EXPIRED);
} else if (e instanceof AccountExpiredException) {//账号已过期
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_EXPIRED);
} else if (e instanceof DisabledException) {//账号已禁用
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.ACCOUNT_IS_DISABLED);
} else {//用户名或密码错误
ResponseUtil.printOut(httpServletResponse, HttpServletResponse.SC_BAD_REQUEST, ResultEnum.USERNAME_OR_PASSWORD_ERROR);
}
}
}
3.4配置Security
- 加过滤器加入到Security中
package com.cz.security;
import com.cz.security.filter.CodeAuthenticationFilter;
import com.cz.security.handler.*;
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.method.configuration.EnableGlobalMethodSecurity;
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.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private CustomAuthFailureHandler customAuthFailureHandler;
@Autowired
private CustomAuthSuccessHandler customAuthSuccessHandler;
@Autowired
private CustomAuthExpiredHandler customAuthExpiredHandler;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Autowired
private CodeAuthenticationFilter codeAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//注入自定义的用户加载类
auth.userDetailsService(userDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/login","/login/code").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(customAuthSuccessHandler)
.failureHandler(customAuthFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(customAuthExpiredHandler)
.accessDeniedHandler(customAccessDeniedHandler)
.and()
.logout()
.logoutSuccessHandler(customLogoutSuccessHandler)
.and()
.addFilterBefore(codeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);
}
}
-
其他类
-
ValidateCodeException
package com.cz.exception;
import com.cz.enums.ResultEnum;
import org.springframework.security.core.AuthenticationException;
public class ValidateCodeException extends AuthenticationException {
private ResultEnum resultEnum;
public ValidateCodeException(ResultEnum resultEnum) {
super(resultEnum.getErrMsg());
this.resultEnum = resultEnum;
}
public ResultEnum getResultEnum() {
return resultEnum;
}
}
ResultEnum
package com.cz.enums;
import lombok.Getter;
@Getter
public enum ResultEnum {
SUCCESS(1, "success"),
SERVER_ERROR(500,"服务器异常!"),
/**
* 登录异常码
*/
USERNAME_OR_PASSWORD_ERROR(800,"用户名或密码错误"),
ACCOUNT_IS_LOCKED(801,"账号被锁定,请联系管理员"),
PASSWORD_IS_EXPIRED(802,"密码已过期,请联系管理员"),
ACCOUNT_IS_EXPIRED(803,"账号已过期,请联系管理员"),
ACCOUNT_IS_DISABLED(804,"账号被禁用,请联系管理员"),
CODE_ERROR_INPUT(805,"验证码输入错误"),
CODE_IS_EXPIRED(806,"验证码已过期"),
LOGIN_IS_EXPIRED(1000,"登录失效"),
NO_AUTH_ACCESS(1001,"无权访问,请联系管理员"),
PARAM_ERROR(1002, "参数错误"),
FILE_PARAM_ERROR(1003, "文件不能超过100M"),
;
private int code;
private String errMsg;
ResultEnum(int code, String errMsg) {
this.code = code;
this.errMsg = errMsg;
}
}
4. 会话保持
-
框架默认通过
session
保持会话,登录成功后,会在服务端内存里保存session
会话,在前端cookie
保存JSESSIONID
-
如果是单机玩,那么认证部分就已经实现完成了
-
如果部署集群,那需要改造了。
4.1 session共享
-
将session会话交由第三方存储,达到集群session可以共享,那么就摆脱了单机
-
这里我们使用redis来存储session,实现session共享
-
添加依赖
<!-- Spring redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring session redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
application.yml
增加配置
spring:
redis:
database: 1
host: 127.0.0.1
port: 6379
timeout: 6000ms
password: 123456
session:
store-type: redis
- 自此,完成!相当简单。启动测试。进行登录,登录成功后,查看redis
- 重启后端服务,在访问需要认证的接口,发现不需要再登录了。
4.2 普通token
- 不再依赖session-cookie机制,使用无状态restful接口风格,那么就要使用到令牌的方式了
4.2.1 封装登录用户类
LoginUser
package com.cz.security.bean;
import com.alibaba.fastjson.annotation.JSONField;
import com.cz.entity.SysDept;
import com.cz.entity.SysRole;
import com.cz.entity.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.List;
/**
* 用户封装类
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public class LoginUser implements UserDetails, Serializable {
private static final long serialVersionUID = -4504630943184309233L;
private SysUser user;
private List<SysRole> roles;
private SysDept sysDept;
private List<String> authority;
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
@Override
@JSONField(serialize = false)
public String getPassword() {
return user.getPassword();
}
@Override
@JSONField(serialize = false)
public String getUsername() {
return user.getUsername();
}
@Override
@JSONField(serialize = false)
public boolean isAccountNonExpired() {
return true;
}
@Override
@JSONField(serialize = false)
public boolean isAccountNonLocked() {
return true;
}
@Override
@JSONField(serialize = false)
public boolean isCredentialsNonExpired() {
return true;
}
@Override
@JSONField(serialize = false)
public boolean isEnabled() {
return user.getEnabled();
}
}
4.2.2 用户登录成功后缓存到redis
LoginUserCache
package com.cz.cache;
import com.alibaba.fastjson.JSONObject;
import com.cz.security.bean.LoginUser;
import com.cz.utils.RedisUtil;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @className: LoginUserCache
* @description: 登录用户信息缓存
* @author: chaizi
* @date: 2021/12/12
**/
@Component
public class LoginUserCache {
@Autowired
private RedisUtil redisUtil;
private final String PREFIX = "login.user:";
private final int EXPIRED = 2;
private String genKey(String key) {
return PREFIX + key;
}
public void setLoginUser(String token, Object loginUser) {
String key = genKey(token);
redisUtil.set(key, JSONObject.toJSONString(loginUser), EXPIRED, TimeUnit.HOURS);
}
public LoginUser getLoginUser(String token) {
String key = genKey(token);
String str = redisUtil.get(key);
if (Strings.isBlank(str)) {
return null;
}
Long expire = redisUtil.getExpire(key);
if (expire < 2000) { //续期token
redisUtil.expire(key, EXPIRED, TimeUnit.HOURS);
}
return JSONObject.parseObject(str, LoginUser.class);
}
public boolean clearUser(String token) {
String key = genKey(token);
return redisUtil.delete(key);
}
}
- 登录成功后
/**
* 登录成功处理器
*
* @author chaizi
* @date 2021年05月12日
*/
@Component
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private LoginUserCache loginUserCache;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//生成token
String token = CodeGeneratedUtil.genMD5(new StringBuilder(authentication.getName()).append(System.currentTimeMillis()).toString());
loginUserCache.setLoginUser(token, authentication.getPrincipal());//缓存到redis
Map result = MapUtil.builder().put("token",token).put("userInfo",authentication.getPrincipal()).build();
ResponseUtil.printOutData(httpServletResponse, HttpServletResponse.SC_OK, ResultEnum.SUCCESS, result);
}
}
4.2.3 编写token过滤器,获取到有效token后设置security的上下文,使其为登录状态,token无效的话,交给后面的过滤器处理
package com.cz.security.filter;
import com.cz.cache.LoginUserCache;
import com.cz.security.bean.LoginUser;
import com.cz.security.exception.ValidateCodeException;
import com.cz.security.handler.CustomAuthFailureHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* token验证过滤器
*/
@Slf4j
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private LoginUserCache loginUserCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (Strings.isBlank(authHeader) || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String authToken = authHeader.substring("Bearer ".length());
LoginUser loginUser = loginUserCache.getLoginUser(authToken);
if (loginUser == null) {
filterChain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
authenticationToken.setDetails(loginUser);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
4.2.4 将过滤器添加到security中,禁用session,不再依赖服务器状态
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//禁用session
.and()
.addFilterAt(customUsernameAndPasswordAuthFilter(), UsernamePasswordAuthenticationFilter.class)
启动,测试,登录成功后,返回token,用token请求其他接口,测试成功!
自此security篇结束,后续有时间补充JWT内容。