Spring Security + JWT实现登录

Spring Security + JWT实现登录

一、实现思路

登录

​ ① 自定义登录接口 —> 通过调用ProviderManager验证是否登录成功 —> 成功后存Redis

​ ② 自定义实现UserDetailService接口,在这个实现类中查询数据库

校验

​ 定义JWT认证过滤器,解析token,获取其中的userId,从Redis中获取用户信息,存入SecurityContextHolder中

二、代码实现

① 流程:

​ a. 实现UserDetailService接口,重写loadUserByUserName()方法,从数据库获取用户的账号信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);
        // 如果没有查询到用户,就抛出异常
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("账号或密码错误!");
        }
        // todo 查询对应的权限


        // 把数据封装并返回
        LoginUser loginUser = new LoginUser();
        loginUser.setUser(user);

        return loginUser;
    }
}

​ b. 新建一个Security的配置类SecurityConfiguration,继承WebSecurityConfigurationadAdapter,在配置类中创及哦按方法passwordEncoder()方法,返回一个加密方式的对象,通常是BCryptPasswordEncoder。并将方法注册进Spring容器,用于替换默认的加密方式。

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    /**
     * 创建BCryptPasswordEncoder注入到容器
     * 使用 BCryptPasswordEncoder 替换 默认的PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

​ c. 在配置类中重写authenticationManager()方法,直接调用其父类方法,但是要将此方法注册到Spring容器中,方便service中调用。实际上这一步的目的就是将其注册到Spring容器中。

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;


    /**
     * 创建BCryptPasswordEncoder注入到容器
     * 使用 BCryptPasswordEncoder 替换 默认的PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份验证
     */
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }
}

​ d. 创建登录LoginController类和LoginService类,并分别常见登陆接口和login()方法,在login()方法中,首先利用AuthenticationManager的authenticate()方法进行身份认证,在认证通过后,生产JWT,并将用户信息封装到loginUser后存入Redis中。

@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public String login(User user) {
        // AuthenticationManager 的authenticate()方法进行身份认证
        UsernamePasswordAuthenticationToken authenticationToken = 
          			new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        // 如果认证没通过
        if (ObjectUtils.isEmpty(authenticate)) {
            throw new RuntimeException("账号或密码错误!");
        }

        // 验证通过, 获取到loginUser, 使用userId 生产一个jwt返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String jwt = JwtUtil.createJWT(loginUser.getUser().getId().toString());

        // 将用户信息存入Redis
        redisCache.setCacheObject("loginUser-" + loginUser.getUser().getId(), JSON.toJSONString(loginUser.getUser()));

        return jwt;
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private User user;
    /**
     * 获取权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
  
    /**
     * 获取密码
     */
    @Override
    public String getPassword() {
        return user.getPassword();
    }

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

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

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

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

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

​ e. 新建一个拦截器,拦截所有的web请求,检查请求中是否携带token,如果带了token,就进行一些列操作后放行,没带就直接放行。一些列操作包括:i. 获取携带的token信息;ii. 解析token得到userId(解析不出来就抛错:token非法!);iii. 根据解析的userId到Redis中获取用户信息,封装进loginUser(如果用户信息不存在,抛错:用户未登录!)。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    public RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        // 如果携带了token, 就做一系列操作, 否则直接放行
        if (StringUtils.hasText(token)) {
            // 解析token
            String userId;
            try {
                Claims claims = JwtUtil.parseJWT(token);
                userId = claims.getSubject();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("token 非法!");
            }

            // 从Redis中获取用户信息
            User user = JSON.parseObject(JSON.toJSONString(redisCache.getCacheObject("loginUser-" + userId)), User.class);

            if (ObjectUtils.isEmpty(user)) {
                throw new RemoteException("用户未登陆!");
            }
            LoginUser loginUser = new LoginUser();
            loginUser.setUser(user);

            // 存入SecurityContextHolder, 因为后面的filter都是从SecurityContextHolder获取的
            // todo 获取权限列表, 封装到authenticationToken
            List authoritis = null;
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authoritis);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        // 放行
        filterChain.doFilter(request, response);
    }
}

​ f. 在Security配置类中,重写configure()方法,利用参数HttpSecurity控制请求的访问,且要将刚刚自定义的拦截器也插入到拦截器链上,加在身份认证之前。.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;


    /**
     * 创建BCryptPasswordEncoder注入到容器
     * 使用 BCryptPasswordEncoder 替换 默认的PasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份验证
     */
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }


    /**
     * 拦截全部请求, 根据条件放行
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
          
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                //把token校验过滤器添加到过滤器链中, 且在UsernamePasswordAuthenticationFilter之前
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)

                .authorizeRequests()
                // 对于登录接口 允许匿名访问(匿名访问是 不携带token可以访问,携带不能访问)
                .antMatchers("/user/login").anonymous()
                // 任何人都能访问
                .antMatchers("/index").permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()

        ;
    }
}

② 完整demo

https://img.lyy52.wang/uPic/2022-05-22/spring-security.zip

参考资料

B站 https://www.bilibili.com/video/BV1mm4y1X7Hc?spm_id_from=333.337.search-card.all.click

posted @ 2022-05-22 20:17  浪漫主义程序员  阅读(804)  评论(0编辑  收藏  举报