security基于表单登录使用

212 阅读4分钟

前述

随着 Spring Boot 2.7.0 的发布,Spring Security 同样也升级到了 5.7.1,升级之后,原有的WebSecurityConfigurerAdapter 方法正式被弃用了,但是主要玩法变化不大。这几天正在以新版Spring Security为基础,记录一下Spring Security的使用过程以及步骤。

本文对应的源代码已经上传到 Gitee,对应项目为 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>

二、自定义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)));
    }
}

三、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();
    }
}

四、自定义实现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));
    }
}

五、SysPermissionService

/**
 * @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());
    }
}

六、业务接口

/**
 * 登录控制器
 *
 * @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";
    }
}
/**
 * <p>
 * 系统用户 前端控制器
 * </p>
 *
 * @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 "删除成功";
    }
}

七、测试效果图

image.png

image.png

以上只是将关键地方的代码贴出,完整的请查看源码哦~

更多Security笔记

更多安全框架笔记