1. 登录用户细节配置

这是SysUser实体,继承BaseEntity类,该父类只是一个公共实体类的一些通用字段罢了,比如创建时间等。

与SpringSecurity无关。

1.1 SysUser实体

  1. public class SysUser extends BaseEntity
  2. {//BaseEntity只有一些创建时间,更新时间的参数
  3. private static final long serialVersionUID = 1L;
  4. /** 用户ID */
  5. @Excel(name = "用户序号", cellType = Excel.ColumnType.NUMERIC, prompt = "用户编号")
  6. private Long userId;
  7. /** 部门ID */
  8. @Excel(name = "部门编号", type = Excel.Type.IMPORT)
  9. private Long deptId;
  10. /** 用户账号 */
  11. @Excel(name = "登录名称")
  12. private String userName;
  13. /** 用户昵称 */
  14. @Excel(name = "用户名称")
  15. private String nickName;
  16. /** 用户邮箱 */
  17. @Excel(name = "用户邮箱")
  18. private String email;
  19. /** 手机号码 */
  20. @Excel(name = "手机号码")
  21. private String phonenumber;
  22. /** 用户性别 */
  23. @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
  24. private String sex;
  25. /** 用户头像 */
  26. private String avatar;
  27. /** 密码 */
  28. private String password;
  29. /** 盐加密 */
  30. private String salt;
  31. /** 帐号状态(0正常 1停用) */
  32. @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
  33. private String status;
  34. /** 删除标志(0代表存在 2代表删除) */
  35. private String delFlag;
  36. /** 最后登录IP */
  37. @Excel(name = "最后登录IP", type = Excel.Type.EXPORT)
  38. private String loginIp;
  39. /** 最后登录时间 */
  40. @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Excel.Type.EXPORT)
  41. private Date loginDate;
  42. /** 部门对象 */
  43. @Excels({
  44. @Excel(name = "部门名称", targetAttr = "deptName", type = Excel.Type.EXPORT),
  45. @Excel(name = "部门负责人", targetAttr = "leader", type = Excel.Type.EXPORT)
  46. })
  47. private SysDept dept;
  48. /** 角色对象 */
  49. private List<SysRole> roles;
  50. /** 角色组 */
  51. private Long[] roleIds;
  52. /** 岗位组 */
  53. private Long[] postIds;
  54. /** 角色ID */
  55. private Long roleId;
  56. }

1.2 LoginUser

1.2.1 UserDetail接口

这是SpringSecurity的接口

  1. public interface UserDetails extends Serializable {
  2. Collection<? extends GrantedAuthority> getAuthorities();//角色表,返回null,因为有其他更好的方式
  3. String getPassword();
  4. String getUsername();
  5. boolean isAccountNonExpired(); //账户是否过期?F可用
  6. boolean isAccountNonLocked();//是否解锁?T可用
  7. boolean isCredentialsNonExpired();//凭据是否过期 T可用
  8. boolean isEnabled();//账户是否可用 T可用
  9. }

1.2.2 LoginUser登录对象

  • 提供核心用户信息,继承UserDetails接口
  • 值得注意的是,这里的LoginUser中的SysUser字段中,还有一些权限相关的字段
  • 与SpringSecurity相关。
  1. /**
  2. * 登录用户身份权限
  3. *
  4. * @author ruoyi
  5. */
  6. @Data
  7. public class LoginUser implements UserDetails
  8. {
  9. private static final long serialVersionUID = 1L;
  10. /**
  11. * 用户ID
  12. */
  13. private Long userId;
  14. /**
  15. * 部门ID
  16. */
  17. private Long deptId;
  18. /**
  19. * 用户唯一标识
  20. */
  21. private String token;
  22. /**
  23. * 登录时间
  24. */
  25. private Long loginTime;
  26. /**
  27. * 过期时间
  28. */
  29. private Long expireTime;
  30. /**
  31. * 登录IP地址
  32. */
  33. private String ipaddr;
  34. /**
  35. * 登录地点
  36. */
  37. private String loginLocation;
  38. /**
  39. * 浏览器类型
  40. */
  41. private String browser;
  42. /**
  43. * 操作系统
  44. */
  45. private String os;
  46. /**
  47. * 权限列表
  48. */
  49. private Set<String> permissions;
  50. /**
  51. * 用户信息
  52. */
  53. private SysUser user;
  54. public LoginUser()
  55. {
  56. }
  57. public LoginUser(SysUser user, Set<String> permissions)
  58. {
  59. this.user = user;
  60. this.permissions = permissions;
  61. }
  62. public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
  63. {
  64. this.userId = userId;
  65. this.deptId = deptId;
  66. this.user = user;
  67. this.permissions = permissions;
  68. }
  69. @JSONField(serialize = false)
  70. @Override
  71. public String getPassword()
  72. {
  73. return user.getPassword();
  74. }
  75. @Override
  76. public String getUsername()
  77. {
  78. return user.getUserName();
  79. }
  80. /**
  81. * 账户是否未过期,过期无法验证
  82. */
  83. @JSONField(serialize = false)
  84. @Override
  85. public boolean isAccountNonExpired()
  86. {
  87. return true;
  88. }
  89. /**
  90. * 指定用户是否解锁,锁定的用户无法进行身份验证
  91. *
  92. * @return
  93. */
  94. @JSONField(serialize = false)
  95. @Override
  96. public boolean isAccountNonLocked()
  97. {
  98. return true;
  99. }
  100. /**
  101. * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
  102. *
  103. * @return
  104. */
  105. @JSONField(serialize = false)
  106. @Override
  107. public boolean isCredentialsNonExpired()
  108. {
  109. return true;
  110. }
  111. /**
  112. * 是否可用 ,禁用的用户不能身份验证
  113. *
  114. * @return
  115. */
  116. @JSONField(serialize = false)
  117. @Override
  118. public boolean isEnabled()
  119. {
  120. return true;
  121. }
  122. @Override
  123. public Collection<? extends GrantedAuthority> getAuthorities()
  124. {
  125. return null;
  126. }
  127. }

1.3 UserDetailsService的业务层

UserDetail接口只是给我们声明自己的对象为UserDetail类型,但是SpringSecurity实际上是调用业务层。

1.3.1 UserDetailsService接口

因此我们需要实现这个业务层接口,只有一个方法。

简单来说,就是从数据取数据,封装成一个LoginUser给SpringSecurity验证

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

1.3.2 UserDetailsService实现类

@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        //从数据库获取User对象
        SysUser user = userService.selectUserByUserName(username);
        //其实如果LoginUser真不可用,SpringSecurity也会抛出异常。这里只是手动判断下,方便控制罢了
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

1.3.3 SysPermissionService 用户权限处理

简言之,就是获取从数据库中获取该用户的权限信息罢了

@Component
public class SysPermissionService
{
    @Autowired
    private ISysRoleService roleService;

    @Autowired
    private ISysMenuService menuService;

    /**
     * 获取角色数据权限
     * 
     * @param user 用户信息
     * @return 角色权限信息
     */
    public Set<String> getRolePermission(SysUser user)
    {
        Set<String> roles = new HashSet<String>();
        // 管理员拥有所有权限,添加为管理员角色
        if (user.isAdmin())
        {
            roles.add("admin");
        }
        else
        {
            roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
        }
        return roles;
    }

    /**
     * 获取菜单数据权限
     * 
     * @param user 用户信息
     * @return 菜单权限信息
     */
    public Set<String> getMenuPermission(SysUser user)
    {
        Set<String> perms = new HashSet<String>();
        // 管理员拥有所有权限
        if (user.isAdmin())
        {
            perms.add("*:*:*");
        }
        else
        {
            perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
        }
        return perms;
    }
}

2.注解方式控制权限

2.1 注解形式

@PreAuthorize(“@ss.hasPermi(‘system:user:edit’)”)

PreAuthorize是SpringSecurity的注解,ss是业务层其中一个类的名字,hasPermi是ss类的方法

  • 即调用ss.hasPermi,如果该方法返回true表示验证通过,否则不通过。

2.1.1 使用方法

在想要控制权限的地方加注解,在Controller中的一个方法加注解即可

@PreAuthorize("@ss.hasPermi('system:user:remove')")
@Log(title = "用户管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds)
{
    if (ArrayUtils.contains(userIds, getUserId()))
    {
        return error("当前用户不能删除");
    }
    return toAjax(userService.deleteUserByIds(userIds));
}

2.1.2 ss类

@Service("ss")
public class PermissionService
{
    /** 所有权限标识 */
    private static final String ALL_PERMISSION = "*:*:*";

    /** 管理员角色权限标识 */
    private static final String SUPER_ADMIN = "admin";

    private static final String ROLE_DELIMETER = ",";

    private static final String PERMISSION_DELIMETER = ",";

    /**
     * 验证用户是否具备某权限
     * 
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission)
    {
        if (StringUtils.isEmpty(permission))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }

    /**
     * 验证用户是否不具备某权限,与 hasPermi逻辑相反
     *
     * @param permission 权限字符串
     * @return 用户是否不具备某权限
     */
    public boolean lacksPermi(String permission)
    {
        return hasPermi(permission) != true;
    }

    /**
     * 验证用户是否具有以下任意一个权限
     *
     * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
     * @return 用户是否具有以下任意一个权限
     */
    public boolean hasAnyPermi(String permissions)
    {
        if (StringUtils.isEmpty(permissions))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        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;
    }

    /**
     * 判断用户是否拥有某个角色
     * 
     * @param role 角色字符串
     * @return 用户是否具备某角色
     */
    public boolean hasRole(String role)
    {
        if (StringUtils.isEmpty(role))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
        {
            return false;
        }
        for (SysRole sysRole : loginUser.getUser().getRoles())
        {
            String roleKey = sysRole.getRoleKey();
            if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * 验证用户是否不具备某角色,与 isRole逻辑相反。
     *
     * @param role 角色名称
     * @return 用户是否不具备某角色
     */
    public boolean lacksRole(String role)
    {
        return hasRole(role) != true;
    }

    /**
     * 验证用户是否具有以下任意一个角色
     *
     * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
     * @return 用户是否具有以下任意一个角色
     */
    public boolean hasAnyRoles(String roles)
    {
        if (StringUtils.isEmpty(roles))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
        {
            return false;
        }
        for (String role : roles.split(ROLE_DELIMETER))
        {
            if (hasRole(role))
            {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断是否包含权限
     * 
     * @param permissions 权限列表
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    private boolean hasPermissions(Set<String> permissions, String permission)
    {
        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
    }
}

3.SpringSecurity配置类

因为AuthenticationManager注入到Spring容器了,我们可以手动获取这个对象,然后手动验证

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/register", "/captchaImage").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .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);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

使用 来验证账号与密码

// 用户验证
Authentication authentication = null;
try
{
    // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
    authentication = authenticationManager
        .authenticate(new UsernamePasswordAuthenticationToken(username, password));
}catch (Exception e)
    ...比如账号密码不正确异常的信息
}