木字楠后台管理系统开发(4):SpringSecurity引入并编写登陆接口

举报
木字楠 发表于 2022/12/12 10:02:09 2022/12/12
【摘要】 Spring Security可以在 Controller层、 Service层、Mapper层等以加注解的方式来保护应用程序的安全。 Spring Security提供了细粒度的权限控制,可以精细到每一个API接口....

木字楠-header.gif

🎶 文章简介:木字楠后台管理系统开发(4):SpringSecurity引入并编写登陆接口

💡 创作目的:为了带大家完整的体验木字楠后台管理系统模版的开发流程

☀️ 今日天气:冬天来啦!

📝 每日一言:用柔软的面点,对抗这个坚硬的世界吧


🧣1、SpringSecurity的认证流程

在使用SpringSecurity之前我们需要了解:

  • 什么是SpringSecurity?
  • SpringSecurity的基本工作原理是什么?

🎒 1-1、什么是SpringSecurity

  1. springsecurity是一个功能强大且高度可定制的身份验证和访问控制框架。
  2. springsecurity是一个专注于为Java应用程序提供身份验证授权的框架。
  3. 与所有Spring项目一样,Spring安全性的真正威力在于它可以很容易地扩展以满足定制需求。

Spring Security可以在 Controller层、 Service层、Mapper层等以加注解的方式来保护应用程序的安全。 Spring Security提供了细粒度的权限控制,可以精细到每一个API接口、每一个业务的方法,或者每一个操作数据库的Mapper层的方法。 Spring Security提供的是应用程序层的安全解决方案,一个系统的安全还需要考虑传输层和系统层的安全,例如采用Htps协议、服务器部署防火墙等。

🎈1-2、SpringSecurity的基本工作原理

Spring Security对Web资源的保护是靠过滤器链(FilterChain)实现的。
当我们发送网络请求至后端服务时,该网络请求会经过一系列的过滤器的过滤,直至通过所有过滤器才可以访问到服务器API。
而我们也可以对这一些列过滤器进行重写,按照我们自己的逻辑来进行过滤,由于SpringSecurity已经帮我们定制的大部分的过滤器,我们仅需要修改少部分过滤器即可完成权限管理。

在这里插入图片描述

💒 2、引入SpringSecurity依赖

本项目中的权限管理使用的是SpringSecurity + Jwt来进行实现权限控制。

    <!--==============  项目版本号规定 ===============-->
    <properties>
        <!--==============  工具依赖  ==================-->
        <userAgent.version>1.21</userAgent.version>
    </properties>
    
    <dependencies>
        <!--==============  SpringBoot相关依赖 ===============-->
        <!-- security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--================== 工具依赖 =======================-->
        <!-- Token生成与解析-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>
    </dependencies>

引入依赖成功之后,我们重新启动项目。
我们会发现控制台中多了一串 字符串

在这里插入图片描述

而当我们访问接口的时候会发现界面变为了一个登陆界面,而非我们的接口返回值。
这是SpringSecurity的登陆拦截,当我们匿名访问接口的时候就会被拦截提示需要登陆,SpringSecurity默认的登陆Usernameuser,而Password则是控制台内打印的 字符串

在这里插入图片描述

我们登陆之后发现接口可以正常访问
但是我们项目中肯定不会使用SpringSecurity的登陆方式,所以我们需要对SpringSecurity进行配置,来以api接口的方式进行登录。

在这里插入图片描述

🍠 3、SpringSecurity配置

我们在config包内新建一个配置类,继承WebSecurityConfigurerAdapter 重写其中的三个config方法

  • @Slf4j用于日志记录
  • @RequiredArgsConstructor 用于构造器注入bean(本项目中不使用@Autowired注入bean)
  • WebSecurityConfigurerAdapter SpringSecurity安全配置类 (Spring Security 5.7.0-M2已经弃用)

在这里插入图片描述

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}

🎁 3-1、网络请求拦截配置

我们对 protected void configure(HttpSecurity http) 进行中的内容进行配置,此方法主要是对于网络请求进行拦截过滤处理。

在这里插入图片描述

🍟 3-1-1、自定义 登录/注销 操作

springsecurity默认的登录界面并不是我们所需要的,我们需要的是一个登录接口。
这里我们配置/user/login为登录接口的接口,/user/logout为注销登录的接口(虽然我们可以修改SpringSecurity的登录方式,修改登录接口。但是由于SpringSecurity底层对登录接口进行了实现,所所以我们只需要去重写其底层实现即可完成自定义登录)

在这里插入图片描述

🌭 3-1-1-1、登录成功处理器

我们查看登录成功处理器发现需要一个 AuthenticationSuccessHandler 类型的接口,我们可以对接口进行实现,自定义一个登录成功处理器。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ResponseResult.success(HttpCodeEnum.USER_LOGIN_SUCCESS)));
    }
}

🍔 3-1-1-2、登录失败处理器

我们查看登录失败处理器发现需要一个 AuthenticationFailureHandler 类型的接口,我们可以对接口进行实现,自定义一个登录失败处理器。但是引起登录失败的原因有很多,所以这里我们需要根据异常进行返回提示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@Slf4j
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        log.info("登录失败 =>" + exception.getMessage());
        response.getWriter().write(JSON.toJSONString(ResponseResult.fail(exception.getMessage())));
    }
}

🌄 3-1-1-3、注销成功处理器

我们查看注销成功处理器发现需要一个 LogoutSuccessHandler 类型的接口,我们可以对接口进行实现,自定义一个注销成功处理器,与登录成功的处理器功能相同。

在这里插入图片描述
在这里插入图片描述

@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ResponseResult.success(HttpCodeEnum.USER_LOGOUT_SUCCESS)));
    }
}

在配置类中进行引入

在这里插入图片描述

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
    private final AuthenticationFailureHandlerImpl authenticationFailureHandler;
    private final LogoutSuccessHandlerImpl logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //region 自定义 登录/注销 处理器
        http.formLogin()
                .loginProcessingUrl("/user/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .and()
                .logout()
                .logoutUrl("/user/logout")
                .logoutSuccessHandler(logoutSuccessHandler);
        //endregion
    }
}

🌅 3-1-2、请求拦截

本项目中默认会拦截所有请求,但是仍有少部分请求时允许匿名访问的,所以这里我们通过使用自定义注解标识的方法来放行允许匿名访问的接口。

  • 匿名访问实现原理:我们在项目启动时获取标记有 @AnonymousAccess 注解的所有方法的请求路径添加入集合中,最终进行统一放行即可。

在这里插入图片描述

新建一个自定义注解,被本注解修饰的方法将会被允许匿名访问。(该注解仅仅时一个标记作用)

在这里插入图片描述

获取被 @AnonymousAccess 注解标记的 RequestMapping的参数,并且统计为一个Set集合。

在这里插入图片描述
在这里插入图片描述

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {

}

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
    private final AuthenticationFailureHandlerImpl authenticationFailureHandler;
    private final LogoutSuccessHandlerImpl logoutSuccessHandler;
    private final ApplicationContext applicationContext;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        //region 自定义 登录/注销 处理器
        http.formLogin()
                .loginProcessingUrl("/user/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .and()
                .logout()
                .logoutUrl("/user/logout")
                .logoutSuccessHandler(logoutSuccessHandler);
        //endregion

        //region 请求拦截
        http.authorizeRequests()
                .antMatchers(listAnonymous().toArray(new String[0])).permitAll()
                .anyRequest().authenticated();
        //endregion
    }

    /**
     * 查找可以匿名访问的接口
     *
     * @return 匿名访问接口集合
     */
    private Set<String> listAnonymous() {
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
        Set<String> anonymousUrls = new HashSet<>();
        anonymousUrls.add("/user/login");
        for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
            HandlerMethod handlerMethod = infoEntry.getValue();
            AnonymousAccess anonymousAccess = handlerMethod.getMethodAnnotation(AnonymousAccess.class);
            if (anonymousAccess != null) {
                assert infoEntry.getKey().getPatternsCondition() != null;
                anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
            }
        }
        log.info("可以匿名访问的url:{}", anonymousUrls);
        return anonymousUrls;
    }
}

❤️ 3-1-3、未登录/未授权处理

上面我们对于网络请求是否允许匿名访问进行了处理,接下来我们对未登录/未授权进行处理。
未登录 <=> 匿名 未授权 <=> 无权限

在这里插入图片描述

🧡 3-1-3-1、未登录处理器

我们查看未登录处理器发现需要一个 AuthenticationSuccessHandler 类型的接口,我们可以对接口进行实现,自定义一个未登录处理器。(注意:未登录处理器 与 登录失败处理器 不冲突)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ResponseResult.fail(HttpCodeEnum.USER_NOT_LOGIN)));
    }
}

🎟️ 3-1-3-2、未授权处理器

我们查看未授权处理器发现需要一个 AuthenticationSuccessHandler 类型的接口,我们可以对接口进行实现,自定义一个未授权处理器。

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ResponseResult.fail(HttpCodeEnum.PERMISSION_NOT_DEFINED)));
    }
}

在这里插入图片描述

🎢 3-2、静态资源拦截配置

我们对 protected void configure(WebSecurity web) 进行中的内容进行配置,此方法主要是对于静态资源拦截过滤处理。我们只需要放行需要放行的静态资源即可。

在这里插入图片描述

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().mvcMatchers(
                "/",
                "/js/**",
                "/css/**",
                "/img/**",
                "/fonts/**",
                "/index.html",
                "/favicon.ico",
                "/doc.html",
                "/swagger-ui.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v3/**",
                "/store/**"
        );
    }

🎪 3-3、登录底层以及密码加密方式配置

我们对 protected void configure(AuthenticationManagerBuilder auth) 进行中的内容进行配置,此方法主要是对于登录实现类的配置以及密码加密方式的配置。

在这里插入图片描述

🎨 3-3-1、登录逻辑重写

SpringSecurity默认的登录逻辑是由接口userDetailsService的实现类执行的,这里我们需要对具体的登录进行重写,所以我们新建一个具体的实现类。(先进行配置,不做具体实现

在这里插入图片描述

🎊 3-3-2、密码加密方式

我们这里选用无法被反向破解的密码加密方式BCrypt加密方式。我们直接将加密方式使用@Bean进行注入。

在这里插入图片描述
在这里插入图片描述

🎏 4、获取用户信息重写

  • 我们需要使用到UserAgent依赖来对请求进行解析,获取请求来源的ip地址、ip来源等信息…

  • loadUserByUsername()方法的实际作用是 根据用户名 来获取用户信息 如果获取到则返回用户实体类,若不存在则直接抛出异常。

  • 若我们在loadUserByUsername()方法中获取到实体类则会对方法进行层层封装,最终的密码校验在 AbstractUserDetailsAuthenticationProvider 中执行。若校验成功,则会去执行登录成功处理器中的内容。若校验失败,则会去执行登录失败处理器中的内容。

在这里插入图片描述

    <!--==============  项目版本号规定 ===============-->
    <properties>
        <!--==============  工具依赖  ==================-->
        <userAgent.version>1.21</userAgent.version>
    </properties>

    <dependencies>
        <!-- 解析客户端操作系统、浏览器等 -->
        <dependency>
            <groupId>eu.bitwalker</groupId>
            <artifactId>UserAgentUtils</artifactId>
            <version>${userAgent.version}</version>
        </dependency>
    </dependencies>

具体的登录逻辑重写如下:

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final HttpServletRequest request;
    private final UserAuthService userAuthService;
    private final UserInfoService userInfoService;
    private final RoleService roleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAuth userAuth = getUserAuthInfo(username);
        UserInfo userInfo = getUserBasicInfo(userAuth.getUserInfoId());
        return convertToUser(request, userAuth, userInfo);
    }


    /**
     * 用户数据转换
     *
     * @param request  request请求
     * @param userAuth 用户权限信息
     * @param userInfo 用户基础信息
     * @return {@link User} 封装后的用户信息
     */
    private User convertToUser(HttpServletRequest request, UserAuth userAuth, UserInfo userInfo) {

        //region 获取请求用户ip地址相关信息
        String ipAddress = UserAgentUtil.getIpAddress(request);
        String ipSource = UserAgentUtil.getIpSource(ipAddress);
        UserAgent userAgent = UserAgentUtil.getUserAgent(request);
        String browser = userAgent.getBrowser().getName();
        String os = userAgent.getOperatingSystem().getName();
        //endregion

        //region 查询用户角色信息并查询对应权限列表
        Role role = roleService.getOne(new LambdaQueryWrapper<Role>().eq(Role::getId, userAuth.getUserRoleId()));
        Optional.ofNullable(role).orElseThrow(() -> new BaseException(HttpCodeEnum.USER_IDENTITY_LOAD_FAIL));
        Set<String> permissionList = roleService.listRolePermission(role);
        //endregion

        return new User() {{
            setId(userAuth.getId());
            setUsername(userAuth.getUsername());
            setPassword(userAuth.getPassword());
            setNickname(userInfo.getNickname());
            setRole(role);
            setLoginType(userAuth.getLoginType());
            setAvatar(userInfo.getAvatar());
            setGender(userInfo.getGender());
            setPersonIntro(userInfo.getPersonIntro());
            setIpAddress(ipAddress);
            setIpSource(ipSource);
            setGmtCreate(userInfo.getGmtCreate());
            setGmtUpdate(userInfo.getGmtUpdate());
            setLastLoginTime(userInfo.getLastLoginTime());
            setEmailLogin(userAuth.getEmailLogin());
            setIsDisabled(userAuth.getIsDisabled());
            setBrowser(browser);
            setOs(os);
            setPermissionList(permissionList);
        }};
    }


    /**
     * 根据用户名查询用户权限信息
     *
     * @param username 用户名
     * @return {@link  UserAuth} 用户权限信息
     */
    private UserAuth getUserAuthInfo(String username) {
        // 用户名非空校验
        if (StringUtils.isEmpty(username)) {
            throw new BaseException(HttpCodeEnum.USERNAME_OR_PASSWORD_ERROR);
        }

        //region 用户权限信息查询并校验
        UserAuth userAuth = userAuthService.getOne(new LambdaQueryWrapper<UserAuth>()
                .eq(UserAuth::getUsername, username)
                .eq(UserAuth::getIsDeleted, 0));
        Optional.ofNullable(userAuth).orElseThrow(() -> new BaseException(HttpCodeEnum.USERNAME_OR_PASSWORD_ERROR));
        if (userAuth.getIsDisabled()) {
            throw new BaseException(HttpCodeEnum.ACCOUNT_IS_DISABLED);
        }
        //endregion

        return userAuth;
    }


    /**
     * 根据用户信息Id查询用户权限信息
     *
     * @param userInfoId 用户信息Id
     * @return {@link  UserInfo} 用户信息
     */
    private UserInfo getUserBasicInfo(Long userInfoId) {
        //region 用户基础信息查询并校验
        UserInfo userInfo = userInfoService.getOne(new LambdaQueryWrapper<UserInfo>()
                .eq(UserInfo::getId, userInfoId));
        Optional.ofNullable(userInfo).orElseThrow(() -> new BaseException(HttpCodeEnum.USER_INFO_LOAD_FAIL));
        //endregion

        return userInfo;
    }
}

RoleServiceImpl

    @Override
    public Set<String> listRolePermission(Role role) {
        Set<String> permissionList = new HashSet<>();

        //region 通过校验则为超级管理员
        if ("super_admin".equals(role.getRoleLabel())) {
            permissionList.add("**:**:**");
            return permissionList;
        }
        //endregion

        return roleMapper.listPermissionByRoleId(role.getId());
    }

RoleMapper.xml

    <select id="listPermissionByRoleId" resultType="java.lang.String">
        select m.perm
        from role
                 left join role_menu rm on role.id = rm.role_id
                 left join menu m on rm.menu_id = m.id
        where role_id = #{roleId}
    </select>

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。