1.新建工程,添加依赖,添加配置
<!-- springbot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springbot thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- MybatisPlus 核心库 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- 引入阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<!-- StringUtilS工具 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
server:
port: 80
spring:
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.22.109:3306/spring_security_demo?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: base
password: base
type: com.alibaba.druid.pool.DruidDataSource
# JWT配置
jwt:
# 密匙
secret: JWTTokenSecret
# 名称
tokenName: jwt_token
# 前缀
tokenPrefix: pJrwitx-
# 过期时间 : 单位秒
expirationTime: 86400
# 不需要认证的接口
antMatchers: /login/**,/logout,/favicon.ico
# Mybatis-plus相关配置
mybatis-plus:
# xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
mapper-locations: classpath:mapper/*.xml
# 以下配置均有默认值,可以不设置
global-config:
db-config:
#主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
#字段策略 IGNORED:"忽略判断" NOT_NULL:"非 NULL 判断") NOT_EMPTY:"非空判断"
field-strategy: NOT_EMPTY
#数据库类型
db-type: MYSQL
configuration:
# 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
map-underscore-to-camel-case: true
# 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏
call-setters-on-nulls: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
2.数据库创建,数据导入
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限名称',
`permission` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限标识',
PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 87 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '权限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, '查看用户信息', 'sys:user:info');
INSERT INTO `sys_menu` VALUES (2, '查看所有权限', 'sys:menu:info');
INSERT INTO `sys_menu` VALUES (3, '查看所有角色', 'sys:role:info');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`role_id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色名称',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ADMIN');
INSERT INTO `sys_role` VALUES (2, 'USER');
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`role_id` bigint(11) NULL DEFAULT NULL COMMENT '角色ID',
`menu_id` bigint(11) NULL DEFAULT NULL COMMENT '权限ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色与权限关系表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (1, 1, 1);
INSERT INTO `sys_role_menu` VALUES (2, 1, 2);
INSERT INTO `sys_role_menu` VALUES (3, 1, 3);
INSERT INTO `sys_role_menu` VALUES (4, 2, 1);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`user_id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`status` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态 PROHIBIT:禁用 NORMAL:正常',
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$5T851lZ7bc2U87zjt/9S6OkwmLW62tLeGLB2aCmq3XRZHA7OI7Dqa', 'NORMAL');
INSERT INTO `sys_user` VALUES (2, 'user', '$2a$10$szHoqQ64g66PymVJkip98.Fap21Csy8w.RD8v5Dhq08BMEZ9KaSmS', 'NORMAL');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(11) NULL DEFAULT NULL COMMENT '用户ID',
`role_id` bigint(11) NULL DEFAULT NULL COMMENT '角色ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户与角色关系表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2, 2);
SET FOREIGN_KEY_CHECKS = 1;
3.添加实体类,service,dao
①实体类:SysUser
/**
*
* @Description 系统用户实体类
*
*/
@Setter
@Getter
@TableName("sys_user")
public class SysUser implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@TableId
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 状态:NORMAL正常 PROHIBIT禁用
*/
private String status;
}
②实体类:SysRole
/**
*
* @Description 系统角色实体类
*
*/
@Setter
@Getter
@TableName("sys_role")
public class SysRole implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 角色ID
*/
@TableId
private Long roleId;
/**
* 角色名称
*/
private String roleName;
}
②实体类:SysMenu
/**
*
* @Description 系统权限实体类
*
*/
@Setter
@Getter
@TableName("sys_menu")
public class SysMenu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 权限ID
*/
@TableId
private Long menuId;
/**
* 权限名称
*/
private String name;
/**
* 权限标识
*/
private String permission;
}
②实体类:SysUserRole,SysRoleMenu
/**
*
* @Description 系统用戶于系統角色关系实体类
*
*/
@Setter
@Getter
@TableName("sys_user_role")
public class SysUserRole implements Serializable {
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 角色ID
*/
private Long roleId;
}
④Dao层
/**
*
* @Description 系统用户DAO层
*
*/
@Mapper
public interface SysUserDao extends BaseMapper<SysUser> {
/**
*
* @Description 根据用户id查询角色集合
*
* @param userId 用户id
* @return List<SysRole> 角色集合
*/
List<SysRole> selectSysRolesByUserId(Long userId);
/**
*
* @Description 根据用户id查询权限集合
*
* @param userId 用户id
* @return List<SysMenu> 权限集合
*/
List<SysMenu> selectSysMenusByUserId(Long userId);
}
<!-- mapper -->
<!-- 查询用户所有角色 -->
<select id="selectSysRolesByUserId" resultType="com.bch.javapro.entity.SysMenu"
parameterType="long">
SELECT sr.* FROM sys_role sr
LEFT JOIN sys_user_role se ON se.role_id = sr.role_id
WHERE se.user_id = #{userId}
</select>
<!-- 查询用户的所有权限 -->
<select id="selectSysMenusByUserId" resultType="com.bch.javapro.entity.SysRole"
parameterType="long">
SELECT DISTINCT m.* FROM sys_user_role ur
LEFT JOIN sys_role_menu rm ON ur.role_id = rm.role_id
LEFT JOIN sys_menu m ON rm.menu_id = m.menu_id
WHERE ur.user_id = #{userId}
</select>
<!-- mapper -->
⑤service层
/**
*
* @Description 系统用户业务层接口
*
*/
public interface SysUserService extends IService<SysUser>{
/**
*
* @Description 根据用户名查询用户实体
*
* @param username 用户名
* @return SysUser 用户实体
*/
SysUser selectUserByUsername(String username);
/**
*
* @Description 根据用户id查询角色集合
*
* @param userId 用户id
* @return List<SysRole> 角色集合
*/
List<SysRole> selectSysRolesByUserId(Long userId);
/**
*
* @Description 根据用户id查询权限集合
*
* @param userId 用户id
* @return List<SysMenu> 权限集合
*/
List<SysMenu> selectMenusByUserId(Long userId);
}
/**
*
* @Description 系统用户业务层实现
*
*/
@Service("sysUserService")
public class SysUserServiceImpl extends ServiceImpl<SysUserDao, SysUser> implements SysUserService{
@Override
public SysUser selectUserByUsername(String username) {
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<SysUser>();
queryWrapper.lambda().eq(SysUser::getUsername, username);
return this.baseMapper.selectOne(queryWrapper);
}
@Override
public List<SysRole> selectSysRolesByUserId(Long userId) {
return this.baseMapper.selectSysRolesByUserId(userId);
}
@Override
public List<SysMenu> selectMenusByUserId(Long userId) {
return this.selectMenusByUserId(userId);
}
}
4.security相关类的实现
①UserDetails的实现类
/**
*
* @Description 自定义SpringSecurity的用户实体类
*
*/
@Data
public class SelfUserDetails implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 状态:NORMAL正常 PROHIBIT禁用
*/
private String status;
/**
* 用户角色
*/
private Collection<GrantedAuthority> authorities;
/**
* 账户是否过期
*/
private boolean isAccountNonExpired = false;
/**
* 账户是否被锁定
*/
private boolean isAccountNonLocked = false;
/**
* 证书是否过期
*/
private boolean isCredentialsNonExpired = false;
/**
* 账户是否有效
*/
private boolean isEnabled = true;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
@Override
public boolean isEnabled() {
return isEnabled;
}
}
②UserDetailsService实现类
/**
*
* @Description 自定义SpringSecurity用户的业务实现类
*
*/
@Component
public class SelfUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
/**
* 获取用户信息
*/
@Override
public SelfUserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
SysUser sysUser = sysUserService.selectUserByUsername(username);
if(null != sysUser) {
SelfUserDetails selfUserDetails = new SelfUserDetails();
BeanUtils.copyProperties(sysUser, selfUserDetails);
return selfUserDetails;
}
return null;
}
}
③自定义登录验证
/**
*
* @Description 自定义登录验证
*
*/
@Component
public class SelfAuthenticationProvider implements AuthenticationProvider {
@Autowired
private SelfUserDetailsService selfUserDetailsService;
@Autowired
private SysUserService sysUserService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 用户名
String username = String.valueOf(authentication.getPrincipal());
// 密码
String password = String.valueOf(authentication.getCredentials());
// 获取用户
SelfUserDetails selfUserDetails = selfUserDetailsService.loadUserByUsername(username);
// 用户不存在
if (null == selfUserDetails) {
throw new UsernameNotFoundException("用户名不存在");
}
// 密码不正确
if (!new BCryptPasswordEncoder().matches(password, selfUserDetails.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
// 用户已被冻结
if (StringUtils.equals("PROHIBIT", selfUserDetails.getStatus())) {
throw new LockedException("该用户已被冻结");
}
// 验证通过,查询用户角色
List<SysRole> sysRoles = sysUserService.selectSysRolesByUserId(selfUserDetails.getUserId());
// 将角色设置给自定义的UserDetails
Set<GrantedAuthority> authorities = new HashSet<>();
if (null != sysRoles && sysRoles.size() > 0) {
for (SysRole sysRole : sysRoles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRole.getRoleName()));
}
}
selfUserDetails.setAuthorities(authorities);
// 登录管理
return new UsernamePasswordAuthenticationToken(selfUserDetails, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
④自定义过滤器,验证token
首先,添加JWT配置类
/**
*
* @Description JWT配置类
*
*/
@Getter
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JWTConfig {
/**
* 密钥
*/
public static String secret;
/**
* 名称
*/
public static String tokenName;
/**
* 前缀
*/
public static String tokenPrefix;
/**
* 过期时间 : 单位秒
*/
public static Long expirationTime;
/**
* 不需要认证的接口
*/
public static String antMatchers;
public void setSecret(String secret) {
JWTConfig.secret = secret;
}
public void setTokenName(String tokenName) {
JWTConfig.tokenName = tokenName;
}
public void setTokenPrefix(String tokenPrefix) {
JWTConfig.tokenPrefix = tokenPrefix;
}
public void setExpirationTime(Long expirationTime) {
JWTConfig.expirationTime = expirationTime;
}
public void setAntMatchers(String antMatchers) {
JWTConfig.antMatchers = antMatchers;
}
}
自定义过滤器如下:
/**
*
* @Description 自定义过滤器,验证token
*
*/
public class SelfAuthenticationFilter extends BasicAuthenticationFilter {
public SelfAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 获取token
String token = request.getHeader(JWTConfig.tokenName);
// token存在,并且前缀正确
if (StringUtils.isNotBlank(token) && token.startsWith(JWTConfig.tokenPrefix)) {
// 截取前缀
token = token.replace(JWTConfig.tokenPrefix, "");
// 解析JWT
Claims claims = Jwts.parser().setSigningKey(JWTConfig.secret).parseClaimsJws(token).getBody();
// 获取用户
String username = claims.getSubject();
String userId = claims.getId();
if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(userId)) {
// 获取角色信息
String authority = claims.get("authorities").toString();
if (StringUtils.isNotBlank(authority)) {
List<GrantedAuthority> authorities = new ArrayList<>();
@SuppressWarnings("unchecked")
List<Map<String, String>> authorityMap = JSONObject.parseObject(authority, ArrayList.class);
for (Map<String, String> role : authorityMap) {
if (StringUtils.isNotBlank(role.get("authority"))) {
authorities.add(new SimpleGrantedAuthority(role.get("authority")));
}
}
// 设置Authentication
SelfUserDetails selfUserDetails = new SelfUserDetails();
selfUserDetails.setUsername(username);
selfUserDetails.setUserId(Long.parseLong(userId));
selfUserDetails.setAuthorities(authorities);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(selfUserDetails, userId, authorities));
}
}
}
chain.doFilter(request, response);
}
}
5.Security的配置(重中之重)
/**
*
* @Description SpringSecurity配置类
*
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SelfAuthenticationProvider selfAuthenticationProvider;
/**
* 加密方式
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 使用自定义的登陆验证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(selfAuthenticationProvider);
}
/**
* 配置控制逻辑
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 不进行验证的资源地址
.antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
// 其他都需要验证
.anyRequest().authenticated().and()
.formLogin()
// 配置登陆地址
// .loginPage("login.html")
// 配置登陆接口地址
.loginProcessingUrl("/login/userLogin")
// 请求成功处理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
SelfUserDetails selfUserDetails = (SelfUserDetails) authentication.getPrincipal();
String token = JWTConfig.tokenPrefix + JWTUtil.createToken(selfUserDetails);
System.out.println("登陆成功:token=" + token);
PrintWriter out = null;
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
out = response.getWriter();
out.println(token);
} finally {
if (null != out) {
out.flush();
out.close();
}
}
}
})
//.successForwardUrl("/hi")
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
System.out.println("请求失败啦");
}
})
.and()
.logout()
// 配置登出地址
.logoutUrl("/logout").and().cors().and().csrf();
// 不使用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 禁用缓存
http.headers().cacheControl();
// 添加自定义过滤器
http.addFilter(new SelfAuthenticationFilter(authenticationManager()));
}
}
配置类使用到的JWTUtil类
/**
*
* @Description JWT工具类
*
*/
public class JWTUtil {
/**
* @Description 私有化构造器
*/
private JWTUtil() {
}
/**
*
* @Description 生成token
*
* @param selfUserDetails 用户实体
* @return token
*/
public static String createToken(SelfUserDetails selfUserDetails) {
String token = Jwts.builder()
.setId(String.valueOf(selfUserDetails.getUserId()))
.setSubject(selfUserDetails.getUsername())
.setIssuedAt(new Date())
.setIssuer("")
.claim("authorities", JSON.toJSONString(selfUserDetails.getAuthorities()))
.setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expirationTime * 1000))
.signWith(SignatureAlgorithm.HS512, JWTConfig.secret)
.compact();
return token;
}
}
6.验证
添加controller
@RestController
public class HiController {
// 所有用户都可以访问
@GetMapping("/hi")
public String hi() {
return "hi";
}
// ADMIN用户都可以访问
@GetMapping("/adminhi")
@PreAuthorize("hasRole('ADMIN')")
public String adminHi() {
return "admin: hi";
}
// USER用户都可以访问
@GetMapping("/userhi")
@PreAuthorize("hasRole('USER')")
public String userHi() {
return "user: hi";
}
}
①浏览器请求http://localhost/hi,发现为登陆,自动调整到登陆画面
②用户登陆,上述sql中admin,user的密码均为123456,获得token
③使用postman访问http://localhost/hi,header添加token
本例使用的是admin用户登陆的,访问http://localhost/adminhi,http://localhost/userhi的结果如下:
补充,权限继承
配置类添加
/**
* 权限继承
*/
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl impl = new RoleHierarchyImpl();
impl.setHierarchy("ROLE_ADMIN > ROLE_USER");
return impl;
}
admin用户访问http://localhost/userhi