security基于表单提交使用

前述

本文对应的源代码已经上传到 Giteeopen in new window,对应项目为 simple ,登录方式为表单登录

后续会持续记录基于 JWT微服务 等场景的实例代码。

基于表单登录的实现

一、引入依赖

这一块我们直接跳到引入依赖,像新建项目这种不再描述

<!-- SpringBoot的版本 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
    <relativePath/> 
</parent>

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--模板引擎-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!--对Thymeleaf添加Spring Security标签支持-->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
</dependencies>

二、其他配置

  • mybatis-plus 以及一些常量工具类的配置描述就不再展开,查看源码即可。

三、自定义security处理器

主要有以下几个

  • 认证失败的处理类
  • 登录成功处理类
  • 自定义并发会话失效策略
  • 退出处理器

认证失败处理类

/**
 * @version 1.0.0
 * @className: AuthenticationEntryPointImpl
 * @description: 认证失败的处理类
 * @author: LiJunYi
 * @create: 2022/7/25 17:39
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8970718410437077606L;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
    {
        int code = HttpStatus.UNAUTHORIZED;
        String msg = StrUtil.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

登录成功处理类

/**
 * @version 1.0.0
 * @className: CustomAuthenticationSuccessImpl
 * @description: 登录成功处理类
 * @author: LiJunYi
 * @create: 2022/7/26 13:30
 */
@Component
public class CustomAuthenticationSuccessImpl implements AuthenticationSuccessHandler
{

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication e) {
        String msg = "登录成功";
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(msg)));
    }
}

并发登录会话失效处理类

/**
 * @version 1.0.0
 * @className: CustomExpiredSessionStrategyImpl
 * @description: 自定义并发会话失效策略
 * @author: LiJunYi
 * @create: 2022/7/26 11:53
 */
@Component
public class CustomExpiredSessionStrategyImpl implements SessionInformationExpiredStrategy , Serializable
{

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException
    {
        int code = HttpStatus.CONFLICT;
        String msg = "您的账号已经在其它设备登录,如果非本人操作,请立即修改密码!";
        HttpServletResponse response = event.getResponse();
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

退出处理类

/**
 * @version 1.0.0
 * @className: LogoutSuccessHandlerImpl
 * @description: 退出处理器
 * @author: LiJunYi
 * @create: 2022/7/26 8:34
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication e) throws IOException {
        boolean isAjaxRequest = ServletUtils.isAjaxRequest(request);
        if (isAjaxRequest)
        {
            ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success("退出成功")));
        }else
        {
            response.sendRedirect("/login");
        }
    }
}

四、SecurityConfig

/**
 * @version 1.0.0
 * @className: SecurityConfig
 * @description: SpringSecurity 5.7.x新用法配置
 * @author: LiJunYi
 * @create: 2022/7/26 8:43
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig
{
    /**
     * 自定义用户登录处理逻辑
     */
    private final MyUserDetailsServiceImpl userDetailsService;

    /**
     * 登录成功处理
     */
    private final CustomAuthenticationSuccessImpl customAuthenticationSuccess;

    /**
     * 注销成功后处理
     */
    private final LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * 未经授权处理程序
     */
    private final AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 并发登录控制处理
     */
    private final CustomExpiredSessionStrategyImpl expiredSessionStrategy;

    /**
     * 构造函数注入
     *
     * @param userDetailsService          用户详细信息服务
     * @param logoutSuccessHandler        注销成功处理程序
     * @param unauthorizedHandler         未经授权处理程序
     * @param expiredSessionStrategy      过期会话策略
     * @param customAuthenticationSuccess 自定义身份验证成功
     */
    @Autowired
    public SecurityConfig(MyUserDetailsServiceImpl userDetailsService,LogoutSuccessHandlerImpl logoutSuccessHandler,
                          AuthenticationEntryPointImpl unauthorizedHandler,
                          CustomExpiredSessionStrategyImpl expiredSessionStrategy,
                          CustomAuthenticationSuccessImpl customAuthenticationSuccess) {
        this.userDetailsService = userDetailsService;
        this.logoutSuccessHandler = logoutSuccessHandler;
        this.unauthorizedHandler = unauthorizedHandler;
        this.expiredSessionStrategy = expiredSessionStrategy;
        this.customAuthenticationSuccess = customAuthenticationSuccess;
    }


    /**
     * 获取AuthenticationManager(认证管理器),登录时认证使用
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        AuthenticationManager authenticationManager = configuration.getAuthenticationManager();
        return configuration.getAuthenticationManager();
    }

    /**
     * 配置加密方式
     *
     * @return {@link PasswordEncoder}
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 侦听器配置,使 Spring Security 更新有关会话生命周期事件的信息
     * 并发会话控制:https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html
     * @return {@link HttpSessionEventPublisher}
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    /**
     * 过滤器链
     *
     * @param http http
     * @return {@link SecurityFilterChain}
     * @throws Exception 异常
     */
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception
    {
        http.authorizeRequests(authorize ->
                authorize.mvcMatchers("/login","/userLogin","/noPermission","/static/css/**","/static/util/javascript/**").permitAll()
                        .anyRequest().authenticated()
        )
                .csrf().disable()
                .formLogin(form -> form
                        .loginPage("/login")
                        .loginProcessingUrl("/userLogin")
                        .successHandler(customAuthenticationSuccess)
                        .permitAll());
        // 退出过滤器
        http.logout(logout -> logout
                .deleteCookies("JSESSIONID")
                .logoutSuccessHandler(logoutSuccessHandler));
        // 认证失败处理类
        http.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).accessDeniedPage("/noPermission");
        // 并发会话控制
        http.sessionManagement(session ->  session
                .maximumSessions(1)
                .expiredSessionStrategy(expiredSessionStrategy));
        http.userDetailsService(userDetailsService);
        return http.build();
    }
}

五、自定义Model实现UserDetails

/**
 * @version 1.0.0
 * @className: LoginUser
 * @description: 自定义用户对象并实现UserDetails
 * @author: LiJunYi
 * @create: 2022/7/26 12:20
 */
public class LoginUser implements UserDetails
{
    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 部门ID
     */
    private Long deptId;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 权限列表
     */
    private Set<String> permissions;

    /**
     * 用户信息
     */
    private SysUser user;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Long getDeptId() {
        return deptId;
    }

    public void setDeptId(Long deptId) {
        this.deptId = deptId;
    }

    public Long getLoginTime() {
        return loginTime;
    }

    public void setLoginTime(Long loginTime) {
        this.loginTime = loginTime;
    }

    public Set<String> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<String> permissions) {
        this.permissions = permissions;
    }

    public SysUser getUser() {
        return user;
    }

    public void setUser(SysUser user) {
        this.user = user;
    }

    public LoginUser()
    {
    }

    public LoginUser(SysUser user, Set<String> permissions)
    {
        this.user = user;
        this.permissions = permissions;
    }

    public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
    {
        this.userId = userId;
        this.deptId = deptId;
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @JSONField(serialize = false)
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     */
    @JSONField(serialize = false)
    @Override
    public boolean isEnabled() {
        return true;
    }
}

六、自定义实现UserDetailsService

/**
 * @version 1.0.0
 * @className: MyUserDetailsServiceImpl
 * @description: security具体登录逻辑
 * @author: LiJunYi
 * @create: 2022/7/26 9:11
 */
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(MyUserDetailsServiceImpl.class);

    private final ISysUserService userService;

    private final SysPermissionService permissionService;

    /**
     * 构造器注入
     *
     * @param userService       用户服务
     * @param permissionService 许可服务
     */
    public MyUserDetailsServiceImpl(ISysUserService userService, SysPermissionService permissionService)
    {
        this.userService = userService;
        this.permissionService = permissionService;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        SysUser user = userService.getByUsername(username);
        if (ObjectUtil.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username + " 不存在");
        }
        // 获取角色
        List<SysRole> roles = permissionService.getRolesByUserId(user);
        user.setRoles(roles);
        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(Convert.toLong(user.getId()), 1000L, user, permissionService.getMenuPermission(user));
    }

}

七、获取登录用户权限

/**
 * @version 1.0.0
 * @className: SysPermissionService
 * @description:
 * @author: LiJunYi
 * @create: 2022/7/26 12:36
 */
@Component
public class SysPermissionService
{
    private final ISysRoleService roleService;
    private final ISysResourceService resourceService;

    /**
     * 构造器注入
     *
     * @param roleService     角色服务
     * @param resourceService 资源服务
     */
    public SysPermissionService(ISysRoleService roleService, ISysResourceService resourceService) {
        this.roleService = roleService;
        this.resourceService = resourceService;
    }

    /**
     * 获取角色数据权限
     *
     * @param user 用户信息
     * @return 角色权限信息
     */
    public Set<String> getRolePermission(SysUser user)
    {
       return roleService.getRolePermissionByUserId(user.getId());
    }

    /**
     * 获取菜单数据权限
     *
     * @param user 用户信息
     * @return 菜单权限信息
     */
    public Set<String> getMenuPermission(SysUser user)
    {
        List<SysResource> resources = resourceService.listResourceByUserId(user.getId());
        List<String> perms = resources.stream().map(SysResource::getPermission).collect(Collectors.toList());
        Set<String> permsSet = new HashSet<>();
        for (String perm : perms)
        {
            if (StrUtil.isNotEmpty(perm))
            {
                permsSet.addAll(Arrays.asList(perm.trim().split(",")));
            }
        }
        return permsSet;
    }

    /**
     * 获取角色通过用户id
     *
     * @param user 用户
     * @return {@link List}<{@link SysRole}>
     */
    public List<SysRole> getRolesByUserId(SysUser user)
    {
        return roleService.getRolesByUserId(user.getId());
    }
}

八、自定义权限校验注解

/**
 * @version 1.0.0
 * @className: PermissionService
 * @description: 自定义的权限校验,代码来源(RuoYi-Vue)项目
 * @author: LiJunYi
 * @create: 2022/7/26 15:38
 */
@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 (StrUtil.isEmpty(permission))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (ObjectUtil.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 (StrUtil.isEmpty(permissions))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (ObjectUtil.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 (StrUtil.isEmpty(role))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (ObjectUtil.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(StrUtil.trim(role)))
            {
                return true;
            }
        }
        return false;
    }

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

    /**
     * 验证用户是否具有以下任意一个角色
     *
     * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
     * @return 用户是否具有以下任意一个角色
     */
    public boolean hasAnyRoles(String roles)
    {
        if (StrUtil.isEmpty(roles))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (ObjectUtil.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(StrUtil.trim(permission));
    }
}

九、登录接口以及业务代码

/**
 * 登录控制器
 *
 * @author LiJunYi
 * @date 2022/07/26
 */
@Controller
public class LoginController
{
    /**
     * 身份验证管理器
     */
    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 登录页面
     *
     * @return {@link String}
     */
    @GetMapping("login")
    public String login() {
        return "login";
    }

    /**
     * 登录方法
     *
     * @param user 用户
     * @return {@link AjaxResult}
     */
    @PostMapping("userLogin")
    @ResponseBody
    public AjaxResult login(SysUser user)
    {
        if (StrUtil.isEmpty(user.getUsername()) || StrUtil.isEmpty(user.getPassword()))
        {
            return AjaxResult.error("用户名或密码未输入!");
        }
        // 用户验证
        Authentication authentication = null;
        try {
            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword()));
        }catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                throw new ServiceException("密码错误");
            }
            else
            {
                throw new ServiceException(e.getMessage());
            }
        }
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        return AjaxResult.success();
    }

    /**
     * 首页
     *
     * @return {@link String}
     */
    @GetMapping("index")
    public String indexPage()
    {
        return "index";
    }

    /**
     * 无权限页面
     *
     * @return {@link String}
     */
    @PostMapping("noPermission")
    public String noPermissionHtml()
    {
        return "noPermission";
    }
}

/**
 * 
 * 系统用户 前端控制器
 * 
 *
 * @author LiJunYi
 * @since 2020-05-25
 */
@Controller
@RequestMapping("/sys/user")
public class SysUserController {
    @Resource
    private ISysUserService userService;

    @PreAuthorize("@ss.hasPermi('sys:user:view')")
    @GetMapping("list")
    public String list(Model model) {
        model.addAttribute("userList", userService.list());
        LoginUser user = SecurityUtils.getLoginUser();
        System.out.println("SecurityUtils.getLoginUser===" + user);
        return "user_list";
    }

    @GetMapping("/add")
    @ResponseBody
    @PreAuthorize("@ss.hasPermi('sys:user:add')")
    public String add() {
        return "跳转新增页面成功";
    }

    @GetMapping("/edit/{id}")
    @PreAuthorize("@ss.hasPermi('sys:user:edit')")
    @ResponseBody
    public String edit(@PathVariable Long id) {
        return "跳转修改页面成功";
    }

    @GetMapping("/del/{id}")
    @PreAuthorize("@ss.hasPermi('sys:user:del')")
    @ResponseBody
    public String del(@PathVariable Long id) {
        return "删除成功";
    }
}

十、测试示例图

SpringSecuritySpringSecuritySpringSecuritySpringSecuritySpringSecurity

以上就是 SpringBoot 接入 SpringSecurity (v5.7.x) 的步骤。

唠嗑

最开始接入的时候,问题还挺多,特别是 securityConfig 配置类这块,虽说变化不大,但是因为官方文档内容比较散,有些配置冲突或者多余也没有标记,还是得自己一步步踩坑过去才知道哪些才是对的配置。

项目源码中,PermissionService 类来源于 RuoYi-Vue 项目。当然你也可以自己实现权限的校验,毕竟登录的时候,权限已经绑定在 loginUser 对象中,只要判断是否包含注解带入的参数即可。

后面一章节将整合jwt,也就是自定义登录过滤器来实现登录,文中有错误的地方希望多多指正。

Last Updated 2024/5/24 16:21:58