概述
Spring是非常流行的Java应用开发框架,Spring Security是基于Spring框架,提供了一套Web应用安全性的完整解决方案。主要有两个方面,用户认证(Authentication)和用户授权(Authorization),用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户授权指的是验证某个用户是否有权限执行某个操作。
SpringSecurity的主要特点如下:
- 和Spring无缝整合
- 全面的权限控制
- 专门为Web开发而设计
- 重量级框架
SpringSecurity更多知识请查阅SpringSecurity官网
Shiro是另一个非常流行的权限控制框架,它是Apache旗下的产品,相对于Spring Security,Shiro有以下特点:
- 轻量级,针对对性能有更高要求的互联网应用有更好的表现
- 通用性,不局限于Web环境,可以脱离Web环境使用,缺陷就是在Web环境下一些特定的需求需要手动编写代码定制
Shiro更多知识请查阅Shiro官网
对于如何选择权限框架,一般来说,常见的技术栈组合如下:
- SSM + Shrio
- Spring Boot/Spring Cloud + Spring Security
若想了解SpringBoot整合SpringSecurity+jwt的步骤,请参阅SpringBoot整合SpringSecurity+Jwt实现权限管理
若想了解SpringBoot整合Shiro的步骤,请参阅SpringBoot2.3.4整合Apache Shiro实现权限管理
相关概念
principal(主体):使用系统的用户或设备或从其他系统远程登录的用户
authentication(认证):权限管理系统确认一个主体的身份,允许主体进入系统,简单的说就”主体”证明自己是谁,也就是我们常说的登录操作
authorization(授权):将系统的”权力”授予给”主体”,这样主体就拥有了操作系统的某些特定功能的能力
UserDetailsService:Spring Security提供的登录逻辑接口,当什么也没有配置的时候,账号和密码是由Spring Security自定义生成的,在实际开发中我们都是需要从数据库中查询出来,这样就需要实现该接口,自定义登录逻辑
UserDetails:上面登录接口返回的用户”主体”,是系统默认的用户,常用的方法有以下几个:
//获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//判断账户是否过期
boolean isAccountNonExpired();
//判断账户是否被锁定
boolean isAccountNonLocked();
//判断密码是否过期
boolean isCredentialsNonExpired();
//判断当前用户是否可用
boolean isEnabled();
在使用的时候可以直接调用其实现类User就可以了,只需要传入用户名、密码和权限即可
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
PasswordEncoder:密码加密,Spring Security提供的密码加密类,默认的密码解析器是BCryptPasswordEncoder,需要在配置文件中实例化,BCryptPasswordEncoder是对bcrypt强散列方法的具体实现,是基于Hash算法实现的单向加密,可以通过strength控制加密强度,默认10
新建表和初始化数据
需要使用的表有:
sys_user:用户表
sys_role:角色表
sys_menu:菜单权限表
sys_user_role:用户角色关联表
sys_role_menu:角色菜单关联表
persistent_logins:记住我登录表(Spring Security可以自动生成改表,用于记住我时保存用户信息)
建表语句和初始化数据如下:
CREATE TABLE `sys_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_name` varchar(50) NOT NULL COMMENT '用户名',
`real_name` varchar(20) NOT NULL COMMENT '真实名称',
`password` varchar(64) NOT NULL COMMENT '密码',
`sex` tinyint(2) NOT NULL DEFAULT '10' COMMENT '性别10:男;11:女;12:其他',
`avatar` varchar(100) DEFAULT NULL COMMENT '头像路径',
`status` tinyint(2) NOT NULL DEFAULT '10' COMMENT '状态10:正常;11:锁定;12:注销',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识0:未删除;1:已删除',
`create_by` varchar(20) DEFAULT NULL COMMENT '创建者',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(20) DEFAULT NULL COMMENT '更新者',
`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(100) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='用户表';
INSERT INTO sys_user (user_name,real_name,password,sex,avatar,status,del_flag,create_by,create_time,update_by,update_time,remark) VALUES
('admin','超级管理员','$2a$10$j3XmUHGzMFLZFH.Qioq4Z.nM/iFMd4Wk6GDI.mC7U2yIztdyV6oUe',10,'',10,0,'admin','2020-09-30 08:34:18.0','admin','2020-09-30 08:34:18.0','')
CREATE TABLE `sys_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_code` varchar(20) NOT NULL COMMENT '角色编号',
`role_name` varchar(30) NOT NULL COMMENT '角色名称',
`status` tinyint(2) unsigned NOT NULL DEFAULT '10' COMMENT '角色状态10:正常;11:停用',
`del_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标识0:未删除;1:已删除',
`create_by` varchar(20) DEFAULT NULL COMMENT '创建者',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(20) DEFAULT NULL COMMENT '更新者',
`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(100) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='角色表';
INSERT INTO sys_role (role_code,role_name,status,del_flag,create_by,create_time,update_by,update_time,remark) VALUES
('admin','超级管理员',10,0,'admin',NULL,'admin',NULL,NULL);
CREATE TABLE `sys_menu` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
`parent_id` bigint(20) unsigned DEFAULT '0' COMMENT '父级菜单ID',
`order_num` tinyint(2) unsigned DEFAULT '0' COMMENT '显示顺序',
`url` varchar(100) NOT NULL DEFAULT '#' COMMENT '请求地址',
`menu_type` tinyint(2) unsigned DEFAULT NULL COMMENT '菜单类型10:目录;20:菜单;30:按钮',
`visible` tinyint(2) unsigned NOT NULL DEFAULT '10' COMMENT '菜单状态10:显示;20:隐藏',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) NOT NULL DEFAULT '#' COMMENT '菜单图标',
`create_by` varchar(20) DEFAULT NULL COMMENT '创建者',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(20) DEFAULT NULL COMMENT '更新者',
`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(100) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8 COMMENT='菜单表';
INSERT INTO sys_menu (menu_name,parent_id,order_num,url,menu_type,visible,perms,icon,create_by,create_time,update_by,update_time,remark) VALUES
('系统管理',0,1,'#',10,10,'','#','admin',NULL,'admin',NULL,NULL)
,('用户管理',1,1,'/system/user',20,10,'user:view','#','admin',NULL,'admin',NULL,NULL)
,('用户查询',10,1,'#',30,10,'user:list','#','admin',NULL,'admin',NULL,NULL)
,('用户新增',10,2,'#',30,10,'user:add','#','admin',NULL,'admin',NULL,NULL)
,('用户修改',10,3,'#',30,10,'user:update','#','admin',NULL,'admin',NULL,NULL)
,('用户删除',10,4,'#',30,10,'user:delete','#','admin',NULL,'admin',NULL,NULL);
CREATE TABLE `sys_user_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户ID',
`role_id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='用户角色表';
INSERT INTO sys_user_role (user_id,role_id) VALUES (1,1);
CREATE TABLE `sys_role_menu` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_id` bigint(20) unsigned NOT NULL COMMENT '角色ID',
`menu_id` bigint(20) unsigned NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8 COMMENT='角色菜单表';
INSERT INTO sys_role_menu (role_id,menu_id) VALUES
(1,1),(1,10),(1,101),(1,102),(1,103),(1,104);
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE = InnoDB CHARACTER SET = utf8;
引入项目依赖
整合SpringSecurity核心的依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置文件
application.yml配置文件如下:
server:
port: 8090
spring:
application:
name: springboot-security
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.108.11:3306/springboot?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: root
hikari:
minimum-idle: 10 #最小空闲连接,默认10
maximum-pool-size: 20 #最大连接数
idle-timeout: 600000 #空闲连接超时时间,默认600000(10分钟)
max-lifetime: 540000 #连接最大存活时间,默认30分钟
connection-timeout: 60000 #连接超时时间,默认30秒
connection-test-query: SELECT 1 #测试连接是否可用查询语句
type: com.zaxxer.hikari.HikariDataSource
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
thymeleaf:
mode: HTML
encoding: UTF-8
cache: false
resources:
static-locations: classpath:/templates/, classpath:/public/, classpath:/static/
mybatis-plus:
mapper-locations: classpath:mapper/**/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
type-aliases-package: com.xlhj.security.entity
logging:
config: classpath:logback-spring.xml
level:
com.xlhj.security: debug
Spring Security关键配置类SecurityConfig代码如下:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Resource
private SecurityAuthenticationFailureHandler authenticationFailureHandler;
@Resource
private AuthenticationEntryPointHandler authenticationEntryPoint;
@Resource
private SecurityAccessDeniedHandler accessDeniedHandler;
/**
* 配置密码解析
* @return
*/
@Bean
protected PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置用户名和密码
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//关闭csrf防护
http.csrf().disable()
//表单登录
.formLogin()
//登录页面
.loginPage("/login.html")
//登录访问路径,与页面表单提交路径一致
.loginProcessingUrl("/login")
//登录成功后访问路径
.defaultSuccessUrl("/index.html").permitAll()
.failureHandler(authenticationFailureHandler)
.and()
//认证配置
.authorizeRequests()
.antMatchers("/login.html", "/login").permitAll()
//配置静态页面可以访问
.antMatchers("/js/**", "/css/**", "/images/**", "/favicon.ico").permitAll()
//任何请求
.anyRequest()
//都需要身份验证
.authenticated();
//配置无权限访问页面
//http.exceptionHandling().accessDeniedPage("/uanuth.html");
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
//配置记住我
http.rememberMe()
//持久层对象
.tokenRepository(persistentTokenRepository())
//失效时间(秒)
.tokenValiditySeconds(60)
//配置自定义登录逻辑
.userDetailsService(userDetailsService);
//配置退出
http.logout()
//退出路径
.logoutUrl("/logout")
//退出后跳转页面
.logoutSuccessUrl("/login.html");
}
/**
* 配置记住我
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true);//请求时创建表
return jdbcTokenRepository;
}
}
自定义登录实现类
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysMenuMapper menuMapper;
@Autowired
private SysRoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", username);
SysUser currentUser = userMapper.selectOne(wrapper);
if (currentUser == null) {
throw new UsernameNotFoundException("用户不存在!");
}
System.out.println(currentUser);
//获取用户角色和菜单权限
List<GrantedAuthority> authorityList = new ArrayList<>();
List<SysRole> roleList = roleMapper.selectRoleCodesByUserId(currentUser.getId());
for (SysRole role : roleList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role.getRoleCode());
authorityList.add(authority);
}
List<SysMenu> permsList = menuMapper.selectMenuPermsByUserId(currentUser.getId());
for (SysMenu perm : permsList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(perm.getPerms());
authorityList.add(authority);
}
//List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(authStr);
return new User(currentUser.getUserName(), currentUser.getPassword(), authorityList);
}
}
控制器代码
@RestController
@RequestMapping("/user")
public class SysUserController {
@GetMapping("/list")
@PreAuthorize("hasAuthority('user:list')")
public ResultData userList() {
return ResultData.ok(ResultCode.SUCCESS, "访问用户查询界面成功!");
}
@GetMapping("/add")
@PreAuthorize("hasAuthority('user:add')")
public ResultData userAdd() {
return ResultData.ok(ResultCode.SUCCESS, "访问用户新增界面成功!");
}
/**
* 测试无权限访问,数据库中权限是user:update
* @return
*/
@GetMapping("/update")
@PreAuthorize("hasAuthority('user:edit')")
public ResultData userUpdate() {
return ResultData.ok(ResultCode.SUCCESS, "访问用户修改界面成功!");
}
@GetMapping("/delete")
@Secured("ROLE_admin")
public ResultData userDelete() {
return ResultData.ok(ResultCode.SUCCESS, "访问用户删除界面成功!");
}
}
页面登录代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post" >
用户名:<input type="text" name="username"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="checkbox" name="remember-me"/>自动登录<br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
注意:页面提交方式必须是post,用户名必须是username,密码必须是password。因为在执行登录逻辑时会调一个过滤器UsernamePasswordAuthenticationFilter,其部分源码如下:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
}
如果需要输入其他字段,必须在配置文件中指定
http.formLogin().usernameParameter("").passwordParameter("")
测试
启动项目,在浏览器输入http://localhost:8090/login.html
输入用户名和密码admin/123456,密码需要提前使用BCryptPasswordEncoder的encode方法生成一个
进入首页
点击有权限的连接会返回如{“success”:true,“code”:20000,“message”:“访问用户查询界面成功!”,“data”:{}}的信息,点击无权限的连接会返回如{“code”:20003,“data”:{},“message”:“没有权限访问!”,“success”:false}信息
权限控制
SpringSecurity提供基于角色或权限进行访问控制,常见的有以下几种方法
hasAuthority
如果当前的主体具有指定权限,则返回true,否则返回false
可以在配置类中配置,也可以在controller的方法中使用注解@PreAuthorize(“hasAuthority(‘user:add’)”)
hasAnyAuthority
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
hasRole
如果用户具备给定角色就允许访问,否则出现403。
如果当前主体具有指定的角色,则返回true。
注意:在给用户添加角色时,要在角色前面加”ROLE_”,在配置类或注解时不需要添加
hasAnyRole
表示用户具备任何一个条件都可以访问
注解使用
@Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“,使用注解先要开启注解功能@EnableGlobalMethodSecurity(securedEnabled = true)
例如:@Secured({“ROLE_normal”,“ROLE_管理员”})
@PreAuthorize
同样在使用时需要开启注解功能@EnableGlobalMethodSecurity(prePostEnabled = true)
适合进入方法前的权限验证,可以将登录用户的roles/permissions参数传到方法中,例如:@PreAuthorize(“hasAnyAuthority(‘menu:system’)”)
@PostAuthorize
使用前需要开启@EnableGlobalMethodSecurity(prePostEnabled = true)
在方法执行后再进行权限验证,适合验证带有返回值的权限
例如:@PostAuthorize(“hasAnyAuthority(‘menu:system’)”)
@PostFilter
权限验证之后对数据进行过滤,表达式中的 filterObject 引用的是方法返回值List中的某一个元素
例如:@PostFilter(“filterObject.username == ‘admin1’”),表示留下用户名是admin1的数据
@PreFilter
进入控制器之前对数据进行过滤
例如:@PreFilter(value = “filterObject.id%2==0”)
完整代码详见码云地址
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76834.html