Spring Security 入门 2
1.项目实战
就以RuoYi-Vue 为例吧,主要以下几点原因:
基于 Spring Security 实现。
基于 RBAC 权限模型,并且支持动态的权限配置。
基于 Redis 服务,实现登录用户的信息缓存。
前后端分离。同时前端采用 Vue ,相对来说后端会 Vue 的比 React 的多。
1.1 表结构
基于 RBAC 权限模型,一共有 5 个表。
实体 | 表 | 说明 |
---|---|---|
SysUser | sys_user | 用户信息 |
SysRole | sys_role | 角色信息 |
SysUserRole | sys_user_role | 用户和角色关联 |
SysMenu | sys_menu | 菜单权限 |
SysRoleMenu | sys_role_menu | 角色和菜单关联 |
5 个表的关系比较简单:
一个 SysUse ,可以拥有多个 SysRole ,通过 SysUserRole 存储关联。
一个 SysRole ,可以拥有多个 SysMenu ,通过 SysRoleMenu 存储关联。
1.1.1 SysUser
SysUser ,用户实体类。代码如下:
// SysUser.javapublic class SysUser extends BaseEntity {private static final long serialVersionUID = 1L;@Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号")private Long userId;@Excel(name = "部门编号", type = Type.IMPORT)private Long deptId;@Excel(name = "登录名称")private String userName;@Excel(name = "用户名称")private String nickName;@Excel(name = "用户邮箱")private String email;@Excel(name = "手机号码")private String phonenumber;@Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")private String sex;/** 用户头像 */private String avatar;/** 密码 */private String password;/** 盐加密 */private String salt;@Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")private String status;/** 删除标志(0代表存在 2代表删除) */private String delFlag;@Excel(name = "最后登录IP", type = Type.EXPORT)private String loginIp;@Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)private Date loginDate;@Excels({@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),@Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)})@Transientprivate SysDept dept;/** 角色对象 */@Transientprivate List<SysRole> roles;/** 角色组 */@Transientprivate Long[] roleIds;/** 岗位组 */@Transientprivate Long[] postIds;// ...省略 set/get 方法}
添加 @Transient 注解的字段,非存储字段。后续的实体,补充重复赘述。
对应表的创建 SQL 如下:
create table sys_user (user_id bigint(20) not null auto_increment comment '用户ID',dept_id bigint(20) default null comment '部门ID',user_name varchar(30) not null comment '用户账号',nick_name varchar(30) not null comment '用户昵称',user_type varchar(2) default '00' comment '用户类型(00系统用户)',email varchar(50) default '' comment '用户邮箱',phonenumber varchar(11) default '' comment '手机号码',sex char(1) default '0' comment '用户性别(0男 1女 2未知)',avatar varchar(100) default '' comment '头像地址',password varchar(100) default '' comment '密码',status char(1) default '0' comment '帐号状态(0正常 1停用)',del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',login_ip varchar(50) default '' comment '最后登录IP',login_date datetime comment '最后登录时间',create_by varchar(64) default '' comment '创建者',create_time datetime comment '创建时间',update_by varchar(64) default '' comment '更新者',update_time datetime comment '更新时间',remark varchar(500) default null comment '备注',primary key (user_id)
) engine=innodb auto_increment=100 comment = '用户信息表';
1.1.2 SysRole
SysRole ,角色实体类。代码如下:
// SysRole.javapublic class SysRole extends BaseEntity {private static final long serialVersionUID = 1L;@Excel(name = "角色序号", cellType = ColumnType.NUMERIC)private Long roleId;@Excel(name = "角色名称")private String roleName;@Excel(name = "角色权限")private String roleKey;@Excel(name = "角色排序")private String roleSort;@Excel(name = "数据范围", readConverterExp = "1=所有数据权限,2=自定义数据权限,3=本部门数据权限,4=本部门及以下数据权限")private String dataScope;@Excel(name = "角色状态", readConverterExp = "0=正常,1=停用")private String status;/** 删除标志(0代表存在 2代表删除) */private String delFlag;/** 用户是否存在此角色标识 默认不存在 */@Transientprivate boolean flag = false;/** 菜单组 */@Transientprivate Long[] menuIds;/** 部门组(数据权限) */@Transientprivate Long[] deptIds;// ...省略 set/get 方法}
对应表的创建 SQL 如下:
create table sys_role (role_id bigint(20) not null auto_increment comment '角色ID',role_name varchar(30) not null comment '角色名称',role_key varchar(100) not null comment '角色权限字符串',role_sort int(4) not null comment '显示顺序',data_scope char(1) default '1' comment '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',status char(1) not null comment '角色状态(0正常 1停用)',del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',create_by varchar(64) default '' comment '创建者',create_time datetime comment '创建时间',update_by varchar(64) default '' comment '更新者',update_time datetime comment '更新时间',remark varchar(500) default null comment '备注',primary key (role_id)
) engine=innodb auto_increment=100 comment = '角色信息表';
1.1.3 SysUserRole
SysUserRole ,用户和角色关联实体类。代码如下:
// SysUserRole.javapublic class SysUserRole {/** 用户ID */private Long userId;/** 角色ID */private Long roleId;// ...省略 set/get 方法}
roleKey 属性,对应的角色标识字符串,可以对应多个角色标识,使用逗号分隔。例如说:“admin,normal” 。
对应表的创建 SQL 如下:
create table sys_user_role (user_id bigint(20) not null comment '用户ID',role_id bigint(20) not null comment '角色ID',primary key(user_id, role_id)
) engine=innodb comment = '用户和角色关联表';
1.1.4 SysMenu
SysMenu ,菜单权限实体类。代码如下:
// SysMenu.javapublic class SysMenu extends BaseEntity {private static final long serialVersionUID = 1L;/** 菜单ID */private Long menuId;/** 菜单名称 */private String menuName;/** 父菜单名称 */private String parentName;/** 父菜单ID */private Long parentId;/** 显示顺序 */private String orderNum;/** 路由地址 */private String path;/** 组件路径 */private String component;/** 是否为外链(0是 1否) */private String isFrame;/** 类型(M目录 C菜单 F按钮) */private String menuType;/** 菜单状态:0显示,1隐藏 */private String visible;/** 权限字符串 */private String perms;/** 菜单图标 */private String icon;/** 子菜单 */@Transientprivate List<SysMenu> children = new ArrayList<SysMenu>();// ...省略 set/get 方法}
menuType 属性,定义了三种类型。其中,F 代表按钮,是为了做页面中的功能级的权限。
perms 属性,对应的权限标识字符串。一般格式为 ${大模块}:${小模块}:{操作}
。示例如下:
用户查询:system:user:query
用户新增:system:user:add
用户修改:system:user:edit
用户删除:system:user:remove
用户导出:system:user:export
用户导入:system:user:import
重置密码:system:user:resetPwd
- 对于前端来说,每个按钮在展示时,可以判断用户是否有该按钮的权限。如果没有,则进行隐藏。当然,前端在首次进入系统的时候,会请求一次权限列表到本地进行缓存。
- 对于后端来说,每个接口上会添加 @PreAuthorize(“@ss.hasPermi(‘system:user:list’)”) 注解。在请求接口时,会校验用户是否有该 URL 对应的权限。如果没有,则会抛出权限验证失败的异常。
- 一个 perms 属性,可以对应多个权限标识,使用逗号分隔。例如说:“system:user:query,system:user:add”
对应表的创建 SQL 如下:
create table sys_menu (menu_id bigint(20) not null auto_increment comment '菜单ID',menu_name varchar(50) not null comment '菜单名称',parent_id bigint(20) default 0 comment '父菜单ID',order_num int(4) default 0 comment '显示顺序',path varchar(200) default '' comment '路由地址',component varchar(255) default null comment '组件路径',is_frame int(1) default 1 comment '是否为外链(0是 1否)',menu_type char(1) default '' comment '菜单类型(M目录 C菜单 F按钮)',visible char(1) default 0 comment '菜单状态(0显示 1隐藏)',perms varchar(100) default null comment '权限标识',icon varchar(100) default '#' comment '菜单图标',create_by varchar(64) default '' comment '创建者',create_time datetime comment '创建时间',update_by varchar(64) default '' comment '更新者',update_time datetime comment '更新时间',remark varchar(500) default '' comment '备注',primary key (menu_id)
) engine=innodb auto_increment=2000 comment = '菜单权限表';
1.1.5 SysRoleMenu
SysRoleMenu ,菜单权限实体类。代码如下:
// SysRoleMenu.javapublic class SysRoleMenu {/** 角色ID */private Long roleId;/** 菜单ID */private Long menuId;// ...省略 set/get 方法}
对应表的创建 SQL 如下:
create table sys_role_menu (role_id bigint(20) not null comment '角色ID',menu_id bigint(20) not null comment '菜单ID',primary key(role_id, menu_id)
) engine=innodb comment = '角色和菜单关联表';
1.2 SecurityConfig
在 SecurityConfig 配置类,继承 WebSecurityConfigurerAdapter 抽象类,实现 Spring Security 在 Web 场景下的自定义配置。代码如下:
// SecurityConfig.java@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// ...}
涉及到的配置方法较多,我们逐个来看看。
重写 #configure(AuthenticationManagerBuilder auth) 方法,实现 AuthenticationManager 认证管理器。代码如下:
// SecurityConfig.java/*** 自定义用户认证逻辑*/
@Autowired
private UserDetailsService userDetailsService;/*** 身份认证接口*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService) // <X>.passwordEncoder(bCryptPasswordEncoder()); // <Y>
}/*** 强散列哈希加密实现*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();
}
<X>
处,调用 AuthenticationManagerBuilder#userDetailsService(userDetailsService) 方法,使用自定义实现的 UserDetailsService 实现类,更加灵活且自由的实现认证的用户信息的读取。在「1.3.1 加载用户信息」中,我们会看到 RuoYi-Vue 对 UserDetailsService 的自定义实现类。<Y>
处,调用 AbstractDaoAuthenticationConfigurer#passwordEncoder(passwordEncoder) 方法,设置 PasswordEncoder 密码编码器。这里,就使用了 bCryptPasswordEncoder 强散列哈希加密实现。
重写 #configure(HttpSecurity httpSecurity) 方法,主要配置 URL 的权限控制。代码如下:
// SecurityConfig.java/*** 认证失败处理类*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;/*** 退出处理类*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;/*** token 认证过滤器*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {httpSecurity// CRSF禁用,因为不使用session.csrf().disable()// <X> 认证失败处理类.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 过滤请求.authorizeRequests()// <Y> 对于登录login 验证码captchaImage 允许匿名访问.antMatchers("/login", "/captchaImage").anonymous().antMatchers(HttpMethod.GET,"/*.html","/**/*.html","/**/*.css","/**/*.js").permitAll().antMatchers("/profile/**").anonymous().antMatchers("/common/download**").anonymous().antMatchers("/swagger-ui.html").anonymous().antMatchers("/swagger-resources/**").anonymous().antMatchers("/webjars/**").anonymous().antMatchers("/*/api-docs").anonymous().antMatchers("/druid/**").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated().and().headers().frameOptions().disable();httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // <Z>// <P> 添加 JWT filterhttpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
比较长,我们选择重点的来看。
<X>
处,设置认证失败时的处理器为 unauthorizedHandler 。详细解析,见「1.6.1 AuthenticationEntryPointImpl」。<Y>
处,设置用于登录的 /login 接口,允许匿名访问。这样,后续我们就可以使用自定义的登录接口。详细解析,见「7.3 登录 API 接口」。<Z>
处,设置登出成功的处理器为 logoutSuccessHandler 。详细解析,见「1.6.3 LogoutSuccessHandlerImpl」。<P>
处,添加 JWT 认证过滤器 authenticationTokenFilter ,用于用户使用用户名与密码登录完成后,后续请求基于 JWT 来认证。 详细解析,见「1.4 JwtAuthenticationTokenFilter」。
重写 #authenticationManagerBean 方法,解决无法直接注入 AuthenticationManager 的问题。代码如下:
// SecurityConfig.java@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();
}
- 在方法上,额外添加了 @Bean 注解,保证创建出 AuthenticationManager Bean 。
下面,我们详细的来看看,各个配置的 Bean 的逻辑。
1.3 登录 API 接口
SysLoginController#login(…)
在 SysLoginController 中,定义了 /login 接口,提供登录功能。代码如下:
// SysLoginController.java@Autowired
private SysLoginService loginService;/*** 登录方法** @param username 用户名* @param password 密码* @param code 验证码* @param uuid 唯一标识* @return 结果*/
@PostMapping("/login")
public AjaxResult login(String username, String password, String code, String uuid) {AjaxResult ajax = AjaxResult.success();// 生成令牌String token = loginService.login(username, password, code, uuid);ajax.put(Constants.TOKEN, token);return ajax;
}
- 在内部,会调用 loginService#login(username, password, code, uuid) 方法,会进行基于用户名与密码的登录认证。认证通过后,返回身份 TOKEN 。
登录成功后,该接口响应示例如下:
{"msg": "操作成功", "code": 200, "token": "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImJkN2Q4OTZiLTU2NTAtNGIyZS1iNjFjLTc0MjlkYmRkNzA1YyJ9.lkU8ot4GecLHs7VAcRAo1fLMOaFryd4W5Q_a2wzPwcOL0Kiwyd4enpnGd79A_aQczXC-JB8vELNcNn7BrtJn9A"
}
后续,前端在请求后端接口时,在请求头上带头该 token 值,作为用户身份标识。
SysLoginService#login(…)
在 SysLoginService 中,定义了 #login(username, password, code, uuid) 方法,进行基于用户名与密码的登录认证。认证通过后,返回身份 TOKEN 。代码如下:
// SysLoginService.java@Autowired
private TokenService tokenService;@Resource
private AuthenticationManager authenticationManager;@Autowired
private RedisCache redisCache;/*** 登录验证** @param username 用户名* @param password 密码* @param code 验证码* @param uuid 唯一标识* @return 结果*/
public String login(String username, String password, String code, String uuid) {// <1> 验证图片验证码的正确性String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; // uuid 的作用,是获得对应的图片验证码String captcha = redisCache.getCacheObject(verifyKey); // 从 Redis 中,获得图片验证码redisCache.deleteObject(verifyKey); // 从 Redis 中,删除图片验证码if (captcha == null) { // 图片验证码不存在AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));throw new CaptchaExpireException();}if (!code.equalsIgnoreCase(captcha)) { // 图片验证码不正确AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));throw new CaptchaException();}// <2> 用户验证Authentication authentication;try {// 该方法会去调用 UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));} catch (Exception e) {// <2.1> 发生异常,说明验证不通过,记录相应的登录失败日志if (e instanceof BadCredentialsException) {AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();} else {AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new CustomException(e.getMessage());}}// <2.2> 验证通过,记录相应的登录成功日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));// <3> 生成 TokenLoginUser loginUser = (LoginUser) authentication.getPrincipal();return tokenService.createToken(loginUser);
}
<1>
处,验证图片验证码的正确性。该验证码会存储在 Redis 缓存中,通过 uuid 作为对应的标识。生成的逻辑,可以看 CaptchaController 提供的 /captchaImage 接口。<2>
处,调用 Spring Security 的 AuthenticationManager 的 #authenticate(UsernamePasswordAuthenticationToken authentication) 方法,基于用户名与密码的登录认证。在其内部,会调用我们定义的 UserDetailsServiceImpl 的 #loadUserByUsername(String username) 方法,获得指定用户名对应的用户信息。详细解析,见「1.3.1 加载用户信息」。<2.1>
处,发生异常,说明认证不通过,记录相应的登录失败日志。<2.2>
处,未发生异常,说明认证通过,记录相应的登录成功日志。
关于上述日志,我们在「1.7 登录日志」来讲。
<3>
处,调用 TokenService 的 #createToken(LoginUser loginUser) 方法,给认证通过的用户,生成其对应的认证 TOKEN 。这样,该用户的后续请求,就使用该 TOKEN 作为身份标识进行认证。
1.3.1 加载用户信息
在 UserDetailsServiceImpl 中,实现 Spring Security UserDetailsService 接口,实现了该接口定义的 #loadUserByUsername(String username) 方法,获得指定用户名对应的用户信息。代码如下:
// UserDetailsServiceImpl.javaprivate static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);@Autowired
private ISysUserService userService;@Autowired
private SysPermissionService permissionService;@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// <1> 查询指定用户名对应的 SysUserSysUser user = userService.selectUserByUserName(username);// <2> 各种校验if (StringUtils.isNull(user)) {log.info("登录用户:{} 不存在.", username);throw new UsernameNotFoundException("登录用户:" + username + " 不存在");} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {log.info("登录用户:{} 已被删除.", username);throw new BaseException("对不起,您的账号:" + username + " 已被删除");} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {log.info("登录用户:{} 已被停用.", username);throw new BaseException("对不起,您的账号:" + username + " 已停用");}// <3> 创建 Spring Security UserDetails 用户明细return createLoginUser(user);
}public UserDetails createLoginUser(SysUser user) {return new LoginUser(user, permissionService.getMenuPermission(user));
}
<1>
处,调用 ISysUserService 的 #selectUserByUserName(String userName) 方法,查询指定用户名对应的 SysUser 。代码如下:
// SysUserServiceImpl.java
@Autowired
private SysUserMapper userMapper;@Override
public SysUser selectUserByUserName(String userName) {return userMapper.selectUserByUserName(userName);
}// SysUserMapper.XML
<sql id="selectUserVo">select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_statusfrom sys_user uleft join sys_dept d on u.dept_id = d.dept_idleft join sys_user_role ur on u.user_id = ur.user_idleft join sys_role r on r.role_id = ur.role_id
</sql><select id="selectUserByUserName" parameterType="String" resultMap="SysUserResult"><include refid="selectUserVo"/>where u.user_name = #{userName}
</select>
-
通过查询 sys_user 表,同时连接 sys_dept、sys_user_role、sys_role 表,将 username 对应的 SysUser 相关信息都一次性查询出来。
-
返回结果 SysUserResult 的具体定义,实际就是 SysUser 实体类。
-
<2>
处,各种校验。如果校验不通过,抛出 UsernameNotFoundException 或 BaseException 异常。 -
<3>
处,调用 SysPermissionService 的 #getMenuPermission(SysUser user) 方法,获得用户的 SysRoleMenu 的权限标识字符串的集合。代码如下:
// SysPermissionService.java
@Autowired
private ISysMenuService menuService;public Set<String> getMenuPermission(SysUser user) {Set<String> roles = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()) {roles.add("*:*:*"); // 所有模块} else {// 读取roles.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));}return roles;
}// SysMenuServiceImpl.java
@Autowired
private SysMenuMapper menuMapper;@Override
public Set<String> selectMenuPermsByUserId(Long userId) {// 读取 SysMenu 的权限标识数组List<String> perms = menuMapper.selectMenuPermsByUserId(userId);// 逐个,按照“逗号”分隔Set<String> permsSet = new HashSet<>();for (String perm : perms) {if (StringUtils.isNotEmpty(perm)) {permsSet.addAll(Arrays.asList(perm.trim().split(",")));}}return permsSet;
}// SysMenuMapper.xml
<select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">select distinct m.permsfrom sys_menu mleft join sys_role_menu rm on m.menu_id = rm.menu_idleft join sys_user_role ur on rm.role_id = ur.role_idwhere ur.user_id = #{userId}
</select>
虽然代码很长,但是核心的并不多。
- 首先,如果 SysUser 是超级管理员,则其权限标识集合就是 :😗 ,标识可以所有模块的所有操作。
- 然后,查询 sys_menu 表,同时连接 sys_role_menu、sys_user_role 表,将 SysUser 拥有的 SysMenu 的权限标识数组,然后使用 “,” 分隔每个 SysMenu 对应的权限标识。
这里,我们看到最终返回的是 LoginUser ,实现 Spring Security UserDetails 接口,自定义的用户明细。代码如下:
// LoginUser.javapublic class LoginUser implements UserDetails {private static final long serialVersionUID = 1L;/** 用户唯一标识 */private String token;/** 登录时间 */private Long loginTime;/** 过期时间 */private Long expireTime;/** 登录IP地址 */private String ipaddr;/** 登录地点 */private String loginLocation;/** 浏览器类型 */private String browser;/** 操作系统 */private String os;/** 权限列表 */private Set<String> permissions;/** 用户信息 */private SysUser user;// ...省略 set/get 方法,以及各种实现方法}
1.3.2 创建认证 Token
在 TokenService 中,定义了 #createToken(LoginUser loginUser) 方法,给认证通过的用户,生成其对应的认证 Token 。代码如下:
// TokenService.java/*** 创建令牌** @param loginUser 用户信息* @return 令牌*/
public String createToken(LoginUser loginUser) {// <1> 设置 LoginUser 的用户唯一标识。注意,这里虽然变量名叫 token ,其实不是身份认证的 TokenString token = IdUtils.fastUUID();loginUser.setToken(token);// <2> 设置用户终端相关的信息,包括 IP、城市、浏览器、操作系统setUserAgent(loginUser);// <3> 记录缓存refreshToken(loginUser);// <4> 生成 JWT 的 TokenMap<String, Object> claims = new HashMap<>();claims.put(Constants.LOGIN_USER_KEY, token);return createToken(claims);
}
注意,这个方法不仅仅会生成认证 Token ,还会缓存 LoginUser 到 Redis 缓存中。
-
<1>
处,设置 LoginUser 的用户唯一标识,即 LoginUser.token。注意,这里虽然变量名叫 token ,其实不是身份认证的 Token 。 -
<2>
处,调用 #setUserAgent(LoginUser loginUser) 方法,设置用户终端相关的信息,包括 IP、城市、浏览器、操作系统。代码如下:
// TokenService.javapublic void setUserAgent(LoginUser loginUser) {UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));String ip = IpUtils.getIpAddr(ServletUtils.getRequest());loginUser.setIpaddr(ip);loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));loginUser.setBrowser(userAgent.getBrowser().getName());loginUser.setOs(userAgent.getOperatingSystem().getName());
}
<3>
处,调用 #refreshToken(LoginUser loginUser) 方法,缓存 LoginUser 到 Redis 缓存中。代码如下:
// application.yaml
# token配置
token:# 令牌有效期(默认30分钟)expireTime: 30// Constants.java
/*** 登录用户 redis key*/
public static final String LOGIN_TOKEN_KEY = "login_tokens:";// TokenService.java
// 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private int expireTime;@Autowired
private RedisCache redisCache;public void refreshToken(LoginUser loginUser) {loginUser.setLoginTime(System.currentTimeMillis());loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);// 根据 uuid 将 loginUser 缓存String userKey = getTokenKey(loginUser.getToken());redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}private String getTokenKey(String uuid) {return Constants.LOGIN_TOKEN_KEY + uuid;
}
- 缓存的 Redis Key 的统一前缀为 “login_tokens:” ,使用 Login 的用户唯一标识(LoginUser.token)作为后缀。
<4>
处,调用 #createToken(Map<String, Object> claims) 方法,生成 JWT 的 Token 。代码如下:
// application.yaml
# token配置
token:# 令牌秘钥secret: abcdefghijklmnopqrstuvwxyz// TokenService.java
// 令牌秘钥
@Value("${token.secret}")
private String secret;private String createToken(Map<String, Object> claims) {return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, secret).compact();
}
- 这里,我们采用了 jjwt 库。
- 注意,不要把这里生成的 JWT 的 Token ,和我们上面的 LoginUser.token 混淆在一起。
- 因为 LoginUser.token 添加到 claims 中,最终生成了 JWT 的 Token 。所以,后续我们可以通过解码该 JWT 的 Token ,从而获得 claims ,最终获得到对应的 LoginUser.token 。
- 在 JWT 的 Token 中,使用 LoginUser.token 而不是 userId 的好处,可以带来更好的安全性,避免万一 secret 秘钥泄露之后,黑客可以顺序生成 userId 从而生成对应的 JWT 的 Token ,后续直接可以操作该用户的数据。不过,这么做之后就不是纯粹的 JWT ,解析出来的 LoginUser.token 需要查询对应的 LoginUser 的 Redis 缓存。详细的,我们在「7.4 JwtAuthenticationTokenFilter」会看到这个过程。
至此,我们完成了基于用户名与密码的登录认证的整个过程。虽然整个过程的代码有小几百行,不过逻辑还是比较清晰明了的。
1.4 JwtAuthenticationTokenFilter
在 JwtAuthenticationTokenFilter 中,继承 OncePerRequestFilter 过滤器,实现了基于 Token 的认证。代码如下:
// JwtAuthenticationTokenFilter.java@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate TokenService tokenService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {// <1> 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(request);// 如果存在 LoginUser ,并且未认证过if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {// <2> 校验 Token 有效性tokenService.verifyToken(loginUser);// <3> 创建 UsernamePasswordAuthenticationToken 对象,设置到 SecurityContextHolder 中UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}// <4> 继续过滤器chain.doFilter(request, response);}}
<1>
处,调用 TokenService 的 #getLoginUser(request) 方法,获得当前 LoginUser 。代码如下:
// application.yaml
# token配置
token:# 令牌自定义标识header: Authorization// TokenService.jav
// 令牌自定义标识
@Value("${token.header}")
private String header;/*** 获取用户身份信息** @return 用户信息*/
public LoginUser getLoginUser(HttpServletRequest request) {// <1.1> 获取请求携带的令牌String token = getToken(request);if (StringUtils.isNotEmpty(token)) {// <1.2> 解析 JWT 的 TokenClaims claims = parseToken(token);// <1.3> 从 Redis 缓存中,获得对应的 LoginUserString uuid = (String) claims.get(Constants.LOGIN_USER_KEY);String userKey = getTokenKey(uuid);return redisCache.getCacheObject(userKey);}return null;
}private String getToken(HttpServletRequest request) {String token = request.getHeader(header);if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {token = token.replace(Constants.TOKEN_PREFIX, "");}return token;
}private Claims parseToken(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
- <1.1> 处,调用 #getToken(request) 方法,从请求头 “Authorization” 中,获得身份认证的 Token 。
- <1.2> 处,调用 #parseToken(token) 方法,解析 JWT 的 Token ,获得 Claims 对象,从而获得用户唯一标识(LoginUser.token)。
- <1.3> 处,从 Redis 缓存中,获得对应的 LoginUser 。
<2>
处,调用 TokenService 的 #verifyToken(LoginUser loginUser) 方法,验证令牌有效期。代码如下:
// TokenService.java
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;/*** 验证令牌有效期,相差不足 20 分钟,自动刷新缓存** @param loginUser 用户*/
public void verifyToken(LoginUser loginUser) {long expireTime = loginUser.getExpireTime();long currentTime = System.currentTimeMillis();// 相差不足 20 分钟,自动刷新缓存if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {String token = loginUser.getToken();loginUser.setToken(token);refreshToken(loginUser);}
}
- 实际上,这个方法的目的不是验证 Token 的有效性,而是刷新对应的 LoginUser 的缓存的过期时间。
- 考虑到避免每次请求都去刷新缓存的过期时间,所以过期时间不足 20 分钟,才会去刷新。
+<3>
处,手动创建 UsernamePasswordAuthenticationToken 对象,设置到 SecurityContextHolder 中。😈 因为,我们已经通过 Token 来完成认证了。
<4>
处,继续过滤器。
严格来说,RuoYi-Vue 并不是采用的无状态的 JWT ,而是使用 JWT 的 Token 的生成方式。
1.5 权限验证
在「进阶使用」中,我们看到可以通过 Spring Security 提供的 @PreAuthorize 注解,实现基于 Spring EL 表达式的执行结果为 true 时,可以访问,从而实现灵活的权限校验。
在 RuoYi-Vue 中,通过 @PreAuthorize 注解的特性,使用其 PermissionService 提供的权限验证的方法。使用示例如下:
// SysDictDataController.java@PreAuthorize("@ss.hasPermi('system:dict:list')")
@GetMapping("/list")
- 请求 /system/dict/data/list 接口,会调用 PermissionService 的 #hasPermi(String permission) 方法,校验用户是否有指定的权限。
- 为什么这里会有一个 @ss 呢?在 Spring EL 表达式中,调用指定 Bean 名字的方法时,使用 @ + Bean 的名字。在 RuoYi-Vue 中,声明 PermissionService 的 Bean 名字为 ss 。
1.5.1 判断是否有权限
在 PermissionService 中,定义了 #hasPermi(String permission) 方法,判断当前用户是否有指定的权限。代码如下:
// PermissionService.java/*** 所有权限标识*/
private static final String ALL_PERMISSION = "*:*:*";@Autowired
private TokenService tokenService;/*** 验证用户是否具备某权限** @param permission 权限字符串* @return 用户是否具备某权限*/
public boolean hasPermi(String permission) {// 如果未设置需要的权限,强制不具备。if (StringUtils.isEmpty(permission)) {return false;}// 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());// 如果不存在,或者没有任何权限,说明权限验证不通过if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {return false;}// 判断是否包含权限return hasPermissions(loginUser.getPermissions(), permission);
}/*** 判断是否包含权限** @param permissions 权限列表* @param permission 权限字符串* @return 用户是否具备某权限*/
private boolean hasPermissions(Set<String> permissions, String permission) {return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
在 PermissionService 中,定义了 #lacksPermi(String permission) 方法,判断当前用户是否没有指定的权限。代码如下:
// PermissionService.java/*** 验证用户是否不具备某权限,与 hasPermi逻辑相反** @param permission 权限字符串* @return 用户是否不具备某权限*/
public boolean lacksPermi(String permission) {return !hasPermi(permission);
}
在 PermissionService 中,定义了 #hasAnyPermi(String permissions) 方法,判断当前用户是否有指定的任一权限。代码如下:
// PermissionService.javaprivate static final String PERMISSION_DELIMETER = ",";/*** 验证用户是否具有以下任意一个权限** @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表* @return 用户是否具有以下任意一个权限*/
public boolean hasAnyPermi(String permissions) {// 如果未设置需要的权限,强制不具备。if (StringUtils.isEmpty(permissions)) {return false;}// 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());// 如果不存在,或者没有任何权限,说明权限验证不通过if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {return false;}// 判断是否包含指定的任一权限Set<String> authorities = loginUser.getPermissions();for (String permission : permissions.split(PERMISSION_DELIMETER)) {if (permission != null && hasPermissions(authorities, permission)) {return true;}}return false;
}
1.5.2 判断是否有角色
在 PermissionService 中,定义了 #hasRole(String role) 方法,判断当前用户是否有指定的角色。代码如下:
// PermissionService.java/*** 判断用户是否拥有某个角色** @param role 角色字符串* @return 用户是否具备某角色*/
public boolean hasRole(String role) {// 如果未设置需要的角色,强制不具备。if (StringUtils.isEmpty(role)) {return false;}// 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());// 如果不存在,或者没有任何角色,说明权限验证不通过if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {return false;}// 判断是否包含指定角色for (SysRole sysRole : loginUser.getUser().getRoles()) {String roleKey = sysRole.getRoleKey();if (SUPER_ADMIN.contains(roleKey) // 超级管理员的特殊处理|| roleKey.contains(StringUtils.trim(role))) {return true;}}return false;
}
在 PermissionService 中,定义了 #lacksRole(String role) 方法,判断当前用户是否没有指定的角色。代码如下:
// PermissionService.java/*** 验证用户是否不具备某角色,与 isRole逻辑相反。** @param role 角色名称* @return 用户是否不具备某角色*/
public boolean lacksRole(String role) {return !hasRole(role);
}
在 PermissionService 中,定义了 #hasAnyRoles(String roles) 方法,判断当前用户是否有指定的任一角色。代码如下:
// PermissionService.javaprivate static final String ROLE_DELIMETER = ",";/*** 验证用户是否具有以下任意一个角色** @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表* @return 用户是否具有以下任意一个角色*/
public boolean hasAnyRoles(String roles) {// 如果未设置需要的角色,强制不具备。if (StringUtils.isEmpty(roles)) {return false;}// 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());// 如果不存在,或者没有任何角色,说明权限验证不通过if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {return false;}// 判断是否包含指定的任一角色for (String role : roles.split(ROLE_DELIMETER)) {if (hasRole(role)) { // 这里实现有点问题,会循环调用 hasRole 方法,重复从 Redis 中读取当前 LoginUserreturn true;}}return false;
}
1.6 各种处理器
1.6.1 AuthenticationEntryPointImpl
在 AuthenticationEntryPointImpl 中,实现 Spring Security AuthenticationEntryPoint 接口,处理认失败的 AuthenticationException 异常。代码如下:
// AuthenticationEntryPointImpl.java// 认证失败处理类 返回未授权
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {private static final long serialVersionUID = -8970718410437077606L;@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {// 响应认证不通过int code = HttpStatus.UNAUTHORIZED;String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));}}
- 响应认证不通过的 JSON 字符串。
1.6.2 GlobalExceptionHandler
在 GlobalExceptionHandler 中,定义了对 Spring Security 的异常处理。代码如下:
// GlobalExceptionHandler.java@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(AccessDeniedException.class) // 没有访问权限。使用 @PreAuthorize 校验权限不通过时,就会抛出 AccessDeniedException 异常public AjaxResult handleAuthorizationException(AccessDeniedException e) {log.error(e.getMessage());return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");}@ExceptionHandler(AccountExpiredException.class) // 账号已过期public AjaxResult handleAccountExpiredException(AccountExpiredException e) {log.error(e.getMessage(), e);return AjaxResult.error(e.getMessage());}@ExceptionHandler(UsernameNotFoundException.class) // 用户名不存在public AjaxResult handleUsernameNotFoundException(UsernameNotFoundException e) {log.error(e.getMessage(), e);return AjaxResult.error(e.getMessage());}// ... 省略对其它的异常类的处理的方法
}
基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。
1.6.3 LogoutSuccessHandlerImpl
在 LogoutSuccessHandlerImpl 中,实现 Spring Security LogoutSuccessHandler 接口,自定义退出的处理,主动删除 LoginUser 在 Redis 中的缓存。代码如下:
// LogoutSuccessHandlerImpl.java// 自定义退出处理类 返回成功
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {@Autowiredprivate TokenService tokenService;/*** 退出处理*/@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {// <1> 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(request);// 如果有登录的情况下if (StringUtils.isNotNull(loginUser)) {String userName = loginUser.getUsername();// <2> 删除用户缓存记录tokenService.delLoginUser(loginUser.getToken());// <3> 记录用户退出日志AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, "退出成功"));}// <4> 响应退出成功ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));}}
-
<1> 处,调用 TokenService 的 #getLoginUser(request) 方法,获得当前 LoginUser 。
-
<2> 处,调用 TokenService 的 #delLoginUser(String token) 方法,删除 LoginUser 的 Redis 缓存。代码如下:
// TokenService.javapublic void delLoginUser(String token) {if (StringUtils.isNotEmpty(token)) {String userKey = getTokenKey(token);// 删除缓存redisCache.deleteObject(userKey);}
}
-
<3> 处,记录相应的退出成功日志。
-
<4> 处,响应退出成功的 JSON 字符串。
1.7 登录日志
SysLogininfor ,登录日志实体。代码如下:
// SysLogininfor.javapublic class SysLogininfor extends BaseEntity {private static final long serialVersionUID = 1L;@Excel(name = "序号", cellType = ColumnType.NUMERIC)private Long infoId;@Excel(name = "用户账号")private String userName;@Excel(name = "登录状态", readConverterExp = "0=成功,1=失败")private String status;@Excel(name = "登录地址")private String ipaddr;@Excel(name = "登录地点")private String loginLocation;@Excel(name = "浏览器")private String browser;@Excel(name = "操作系统")private String os;@Excel(name = "提示消息")private String msg;@Excel(name = "访问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")private Date loginTime;// ...省略 set/get 方法
}
对应表的创建 SQL 如下:
create table sys_logininfor (info_id bigint(20) not null auto_increment comment '访问ID',user_name varchar(50) default '' comment '用户账号',ipaddr varchar(50) default '' comment '登录IP地址',login_location varchar(255) default '' comment '登录地点',browser varchar(50) default '' comment '浏览器类型',os varchar(50) default '' comment '操作系统',status char(1) default '0' comment '登录状态(0成功 1失败)',msg varchar(255) default '' comment '提示消息',login_time datetime comment '访问时间',primary key (info_id)
) engine=innodb auto_increment=100 comment = '系统访问记录';
在 RuoYi-Vue 中,记录 SysLogininfor 的过程如下:
- 首先,手动调用 AsyncFactory#recordLogininfor(username, status, message, args) 方法,创建一个 Java TimerTask 任务。
- 然后调用 AsyncManager#execute(TimerTask task) 方法,提交到定时任务的线程中,延迟 OPERATE_DELAY_TIME = 10 秒后,存储该记录到数据库中。
这样的好处,是可以实现异步存储日志到数据库中,提升 API 接口的性能。不过实际上,Spring 提供了 @Async 注解,方便的实现异步操作.
另外,在 RuoYi-Vue 中还定义了 SysOperLog ,操作日志实体类。
1.8 获得用户信息 API 接口
在 SysLoginController 中,定义了 /getInfo 接口,获取登录的用户信息。代码如下:
// SysLoginController.java/*** 获取用户信息** @return 用户信息*/
@GetMapping("getInfo")
public AjaxResult getInfo() {// <1> 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());SysUser user = loginUser.getUser();// <2> 角色标识的集合Set<String> roles = permissionService.getRolePermission(user);// <3> 权限集合Set<String> permissions = permissionService.getMenuPermission(user);// <4> 返回结果AjaxResult ajax = AjaxResult.success();ajax.put("user", user);ajax.put("roles", roles);ajax.put("permissions", permissions);return ajax;
}
-
<1>
处,调用 TokenService 的 #getLoginUser(request) 方法,获得当前 LoginUser 。 -
<2>
处,调用 PermissionService 的 #getRolePermission(SysUser user) 方法,获得 LoginUser 拥有的角色标识的集合。代码如下:
// SysPermissionService.java
@Autowired
private ISysRoleService roleService;/*** 获取角色数据权限** @param user 用户信息* @return 角色权限信息*/
public Set<String> getRolePermission(SysUser user) {Set<String> roles = new HashSet<String>();// 管理员拥有所有权限if (user.isAdmin()) { // 如果是管理员,强制添加 admin 角色roles.add("admin");} else { // 如果非管理员,进行查询roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));}return roles;
}// SysRoleServiceImpl.java@Autowired
private SysRoleMapper roleMapper;/*** 根据用户ID查询权限** @param userId 用户ID* @return 权限列表*/
@Override
public Set<String> selectRolePermissionByUserId(Long userId) {// 获得 userId 拥有的 SysRole 数组List<SysRole> perms = roleMapper.selectRolePermissionByUserId(userId);// 遍历 SysRole 数组,生成角色标识数组Set<String> permsSet = new HashSet<>();for (SysRole perm : perms) {if (StringUtils.isNotNull(perm)) {permsSet.addAll(Arrays.asList(perm.getRoleKey().trim().split(",")));}}return permsSet;
}// SysRoleMapper.xml
<sql id="selectRoleVo">select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope,r.status, r.del_flag, r.create_time, r.remark from sys_role rleft join sys_user_role ur on ur.role_id = r.role_idleft join sys_user u on u.user_id = ur.user_idleft join sys_dept d on u.dept_id = d.dept_id
</sql><select id="selectRolePermissionByUserId" parameterType="Long" resultMap="SysRoleResult"><include refid="selectRoleVo"/>WHERE r.del_flag = '0' and ur.user_id = #{userId}
</select>
-
通过查询 sys_role 表,同时连接 sys_user_role、sys_user、sys_dept 表,将 userId 对应的 SysRole 相关信息都一次性查询出来。
-
返回结果 SysRoleResult 的具体定义,点击 传送门 查看,实际就是 SysRole 实体类。
-
<3> 处,调用 SysPermissionService 的 #getMenuPermission(SysUser user) 方法,获得用户的 SysRoleMenu 的权限标识字符串的集合。
-
<4> 处,返回用户信息的 AjaxResult 结果。
通过调用该 /getInfo 接口,前端就可以根据角色标识、又或者权限标识,实现对页面级别的按钮实现权限控制,进行有权限时显示,无权限时隐藏。
1.9 获取路由信息
在 SysLoginController 中,定义了 /getRouters 接口,获取获取路由信息。代码如下:
// SysLoginController.java@GetMapping("getRouters")
public AjaxResult getRouters() {// 获得当前 LoginUserLoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());// 获得用户的 SysMenu 数组SysUser user = loginUser.getUser();List<SysMenu> menus = menuService.selectMenuTreeByUserId(user.getUserId());// 构建路由 RouterVo 数组。可用于 Vue 构建管理后台的左边菜单return AjaxResult.success(menuService.buildMenus(menus));
}
通过调用该 /getRouters 接口,前端就可以构建管理后台的左边菜单。
1.10 权限管理
用户管理 SysUserController :用户是系统操作者,该功能主要完成系统用户配置。
角色管理 SysRoleController :角色菜单权限分配、设置角色按机构进行数据范围权限划分。
菜单管理 SysMenuController :配置系统菜单,操作权限,按钮权限标识等。
相关文章:

Spring Security 入门 2
1.项目实战 就以RuoYi-Vue 为例吧,主要以下几点原因: 基于 Spring Security 实现。 基于 RBAC 权限模型,并且支持动态的权限配置。 基于 Redis 服务,实现登录用户的信息缓存。 前后端分离。同时前端采用 Vue ,相对来…...

C++初阶学习第七弹——探索STL奥秘(二)——string的模拟实现
标准库中的string:C初阶学习第六弹——string(1)——标准库中的string类-CSDN博客 前言: 在前面我们已经学习了如何使用标准库中的string类,但作为一个合格的程序员,我们不仅要会用,还要知道如…...

5.nginx常用命令和日志定时切割
一. nginx常用的相关命令介绍 1.强制关闭nginx: ./nginx -s stop 2.优雅的关闭nginx: ./nginx -s quit 3.检查配置文件是否正确: ./nginx -t 4.查看nginx版本: ./nginx -v 5.查看nginx版本相关的配置环境信息:./nginx -V 6.nginx帮助信…...

Redis-详解(基础)
文章目录 什么是Redis?用Redis的特点?用Redis可以实现哪些功能?Redis的常用数据类型有哪些?Redis的常用框架有哪些?本篇小结 更多相关内容可查看 什么是Redis? Redis(Remote DictionaryServer)是一个开源…...

记录minio的bug(Object name contains unsupported characters.)
场景是我将后端服务从121.xxx.xxx.xxx服务器上转移到了另一台服务器10.xxx.xxx.xxx 但图片都还在121.xxx.xxx.xxx服务器上,同样我10.xxx.xxx.xxx也安装了minio并且我的后端服务配置的minio地址也是10.xxx.xxx.xxx 此时有一个业务通过minio客户端获取图片…...

【嵌入式开发 Linux 常用命令系列 7.6 -- sed 替换指定字符串】
请阅读【嵌入式开发学习必备专栏】 文章目录 sed 替换指定字符串 sed 替换指定字符串 背景: 找到当前目录下所有的.h 和 .c 文件 将他们中的字符 print_log替换为 demo_log 可以使用find命令结合sed命令在Linux环境下完成这项任务。下面是一个命令行示例ÿ…...

C++语言的字符数组
存放字符数据的数组是字符数组,字符数组中的一个元素存放一个字符。字符数组具有数组的共同属性。 1. 声明一个字符数组 char c[5]; 2. 字符数组赋值方式 (1)为数组元素逐一赋值 c[0]H c[1]E c[2]L c[3]L c[4]O (2&…...

24届电信红队实习生面经
sql注入的一些:原理、打的靶场的常见绕过、问了一些函数 (load_file、 outfile这些)、后利用 (mysql的udf提权的原理、条件、利用、其他像mssql这些数据库的提权手段、这些就没细问了, 就问有哪些方式; 问了有没有实战遇到mysql的…...

linux下使用jexus部署aspnet站点
1.运行环境 Centos 7 安装dos2unix工具 yum install dos2unix 安装jexus curl https://jexus.org/release/x64/install.sh|sudo sh2.网站部署 2.1. 将windows下的网站发布包Msc_qingdao_admin.zip上传到linux中, 然后解压后放入/var/www(没有则创建)目录下 r…...

代码随想录训练营Day 27|理论基础、力扣 77. 组合
1.理论基础 题目链接/文章讲解:代码随想录 视频讲解:带你学透回溯算法(理论篇)| 回溯法精讲!_哔哩哔哩_bilibili 来自代码随想录的网站: void backtracking(参数) {if (终止条件) {存放结果;return;}for (…...

Spring框架深度解析:打造你的Java应用梦工厂
想要在Java企业级应用开发中大展身手?Spring框架的核心容器是你不可或缺的伙伴! 文章目录 一. 引言1.1 介绍Spring框架的重要性1.2 阐述核心容器在Spring框架中的作用1.3 故事开端 二. 背景介绍2.1 描述Spring框架的发展历程2.2 概述Spring框架的主要特点…...

Python 正则表达式(一)
文章目录 概念正则函数match函数正则表达式修饰符意义: 常用匹配符限定符原生字符串边界字符 概念 正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个…...

Cocos Creator 3.8.x报错:5302
在小游戏加载某个bundle后,如果报以下错误: 5302:Can not find class %s 说明bundle中某个预制件*.prefab引用了未加载的bundle的资源。 解决方法有两个: 1、将引用的资源移到预制件*.prefab相同的bundle下; 2、将…...

网页如何集成各社区征文活动
Helllo , 我是小恒 由于我需要腾讯云社区,稀土掘金以及CSDN的征文活动RSS,找了一下没发现,所以使用GET 请求接口对网页定时进行拉取清洗,甚至无意间做了一个简单的json格式API 最终网址:hub.liheng.work API:http://hub.liheng.wo…...

【知识碎片】2024_05_13
本文记录了两道代码题【自除数】和【除自身以外数组的乘积】(利用了前缀积和后缀积,值得再看),第二部分记录了关于指针数组和逗号表达式的两道选择题。 每日代码 自除数 . - 力扣(LeetCode) /*** Note: T…...

Day53代码随想录动态规划part13:300.最长递增子序列、674. 最长连续递增序列、718. 最长重复子数组
Day52 动态规划part13 300.最长递增子序列 leetcode链接:300. 最长递增子序列 - 力扣(LeetCode) 题意:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除&a…...

自己动手为wordpress注册一个Carousel轮播区块
要为WordPress注册一个Carousel轮播区块,你可以创建一个自定义Gutenberg块。以下是一个简单的示例,说明如何创建一个Carousel轮播区块: 1. 在你的主题目录中创建一个名为carousel-block的子文件夹。在这个文件夹中,创建一个名为c…...

基于Springboot的实习生管理系统(有报告)。Javaee项目,springboot项目。
演示视频: 基于Springboot的实习生管理系统(有报告)。Javaee项目,springboot项目。 项目介绍: 采用M(model)V(view)C(controller)三层体系结构&a…...

良心实用的电脑桌面便利贴,好用的便利贴便签小工具
在日常办公中,上班族经常需要记录临时任务、重要提醒或者突发的灵感。比如,在紧张的项目会议中,忽然想到一个改进的点子,或者是在处理邮件时,需要记下对某个客户的回复要点。在这些场景下,如果能直接在电脑…...

Eayswoole 报错 crontab info is abnormal
在执行一个指定的定时任务时 如 php easyswoole crontab show 报错 crontab info is abnormal 如下图所示: 查询了半天 修改了如下配置: 旧的 // 创建定时任务实例 $crontab new \EasySwoole\Crontab\Crontab($crontabConfig); 修改后&#…...

移动 App 入侵与逆向破解技术-iOS 篇
如果您有耐心看完这篇文章,您将懂得如何着手进行app的分析、追踪、注入等实用的破解技术,另外,通过“入侵”,将帮助您理解如何规避常见的安全漏洞,文章大纲: 简单介绍ios二进制文件结构与入侵的原理介绍入…...

2024服贸会,参展企业媒体宣传报道攻略
传媒如春雨,润物细无声,大家好,我是51媒体网胡老师。 2024年中国国际服务贸易交易会(简称“服贸会”)是一个重要的国际贸易平台,对于参展企业来说,有效的媒体宣传报道对于提升品牌知名度、扩大…...

CI/CD笔记.Gitlab系列.新用户管理
CI/CD笔记.Gitlab系列 新用户管理 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite:http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_285502…...

前端 JS 经典:JS 基础类型和 typeof
前言:JS 基础类型就 8 种,这是官方确定的,毋庸置疑。其中原始类型 7 种,对象类型 1 种。而 typeof 关键字是用来判断数据是属于什么类型的。 1. 原始类型 Number、Boolean、String、BigInt、symbol、Undefined、null typeof 18…...

Java入门基础学习笔记11——关键字和标识符
1、关键字 关键字是java中已经被赋予特定意义的,有特殊作用的一些单词,不可以把这些单词作为标识符来使用。 注意:关键字是java用了的,我们就不能用来作为:类名、变量名、否则会报错。 标识符: 标识符就是…...

设计模式-解释器模式(Interpreter)
1. 概念 解释器模式(Interpreter Pattern)是一种行为型设计模式,它用于定义一个语言的文法,并解析语言中的表达式。具体来说,解释器模式通过定义一个解释器来解释语言中的表达式,从而实现对语言的解析和执…...

机器视觉任务中语义分割方法的进化历史
机器视觉任务中语义分割方法的进化历史 一、基于传统方法的图像分割二、基于卷积神经网络的图像分割三、基于Attention机制的图像分割四、语义分割模型的挑战与改进 在图像处理领域,传统图像分割技术扮演着重要角色。 一、基于传统方法的图像分割 这些方法包括大津…...

Java并发编程: Synchronized锁升级
文章目录 一、jdk8 markword实现表二、使用工具来查看锁升级三、默认synchronized(o) 一、jdk8 markword实现表 为什么有自旋锁还需要重量级锁: 自旋消耗CPU资源,如果锁的时间长,或者自旋线程多,CPU会被大量消耗。重量…...

Atcoder C - Routing
https://atcoder.jp/contests/arc177/tasks/arc177_c 思路:该问题可以归约为最短路问题,问题中的条件1和条件2是相互独立的,可以分开考虑,从地图中的一个点,沿上下左右四个方向走,所花费的代价为࿱…...

升级! 测试萌新Python学习之连通数据库Pymsql增删改及封装(四)
pymysql 数据库概述python对数据库的增删改查pymysql核心操作事务事务操作pymysql工具类封装每日复习ChatGPT的回答 数据库概述 分类 关系型数据库: 安全 如, mysql oracle SQLite…database tables 行列 非关系型数据库: 高效 如, redis mongoDB…数据存储结构多样 键值对…...