Spring Boot集成Spring Security系列三之自定义登录

1,205 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情

大家好,我是程序员路飞,三年的下水道打杂前端,喜欢研究技术,正在往全栈发展
欢迎小伙伴们加我微信:DZHningmeng,一起讨论,期待与大家共同成长🥂

前言

这是Spring Boot集成Spring Security的第三章,我将会通过这个系列文章,包括完整的项目搭建、编码过程实现(认证和授权),关键点讲解,来帮助大家了解Spring Security在实际项目中的应用

今天这篇内容主要是登录接口和使用用户的认证

源码链接

往期系列

Spring Boot集成Spring Security系列一之搭建项目

Spring Boot集成Spring Security系列二之查询用户

功能实现

Spring Security有内置的一套登录逻辑,我们需要重写对应的方法实现自定义的登录逻辑

重写WebSecurityConfigurerAdapter内置的方法

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    @Autowired
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/login", "/user/register") // 对登录注册要允许匿名访问
                .permitAll()
                .antMatchers(HttpMethod.OPTIONS) // 跨域请求会有一次OPTIONS请求
                .permitAll()
                .antMatchers("/**") // 测试验证,上线需要关闭
                .permitAll()
                .anyRequest() // 除上面的所有请求全部需要鉴权认证
                .authenticated();
        // 禁用缓存
        httpSecurity.headers().cacheControl();
        // 添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        // 添加自定义未授权和未登录结果返回
        httpSecurity.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthenticationEntryPoint);
    }

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

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
        return new JwtAuthenticationTokenFilter();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        // 获取登录用户信息
        return username -> {
            User admin = userService.queryUserByUsername(username);
            if (admin != null) {
                List<UserPermission> permissionList = new ArrayList<>();
                return new AdminUserDetails(admin, permissionList);
            }
            throw new UsernameNotFoundException("用户名或密码错误");
        };
    }
}

这个时候我们再次重新启动项目会发现没有生成密码,然后访问http://localhost:8080/user/queryUserByUsername?username=admin

发现不需要登录就能访问,这是因为我们打开了测试的代码,要注释掉这段代码

.antMatchers("/**") // 测试验证,上线需要关闭
.permitAll()

然后再再次重新启动项目访问接口会报401,这是因为我们没有从登录的生成jwt中获取对应的username,查询到对应权限,所以我们需要写一个登录接口生成jwt返回给前端,然后和一个获取权限的service,才能查询到用户是否有对应接口的权限

image.png

编写登录接口

用户校验用户登录生成jwt返回前端的功能

Controller层

@RequestMapping(value = "/login", method = RequestMethod.POST)
public CommonResult login(@RequestBody UserLoginParam userLoginParam, HttpServletResponse response) {
    String token = userService.login(userLoginParam.getUsername(), userLoginParam.getPassword());
    if(token == null) {
        return CommonResult.validateFailed("用户名或者密码错误");
    }

    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put("tokenHeader", tokenHeader);
    tokenMap.put("token", token);
    response.setHeader(tokenHeader, token);
    return CommonResult.success(tokenMap);
}

ServiceImpl层

@Override
public String login(String username, String password) {
    String token = null;

    try {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if(!passwordEncoder.matches(password, userDetails.getPassword())){
            throw new BadCredentialsException("密码不正确");
        }
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
        token = jwtTokenUtil.generateToken(userDetails);
    } catch (Exception e) {
        LOGGER.warn("登录异常: {}", e.getMessage());
    }
    return token;
}

这个时候启动会报循环依赖的问题,因为SpringSecurity和userServiceImpl循环依赖了,使用需要在配置文件加一个允许Spring循环依赖的配置(上面两个的不会导致其他问题,其他情况需要具体分析会不会引起其他问题)

spring:
  main:
    allow-circular-references: true

再次启动访问接口,这次我们使用IDEA内置的请求工具,增加一个api.http文件

image.png

登录接口,查看返回值,可以查看登录成功

image.png

接下来测试之前开发的查询用户接口Authorization先不加看什么情况,可以看到认证表不通过

image.png

然后加上Authorization: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE2NTAxMzI5MzkwNzgsImV4cCI6MTY1MDczNzczOX0.pB0OMZJg11NqOWSi72G-XY-fN-el9bWS3bxDKyM07bytPXPVaVd-VC4IMYKmoPfO8vkFoe5l-ThhTSnvxYNCCg(上一次请求生成的)

image.png image.png

可以看到接口的请求成功,这样子登录的功能和接口的token解析就完成了,下一篇我们会讲到如果鉴权,就是对接口进行权限管理。

🚴‍♀️ 结束语

通过项目实践,结合具体的步骤,输出相关的文章,给小伙伴一些有些的知识点,希望大家喜欢我的文章,希望认识到更多志同道合的伙伴,如果你也对技术感兴趣,可以加我好友互相探讨一起进步

Github: Cheering-baby

公众号: 程序员路飞

vx: DZHningmeng

最后,如果喜欢我的文章,可以给个赞👍或者关注➕都是对我最大的支持