SpringSecurity快速入门(二)

163 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

image.png

上回书说到 SpringSecurity的原理其实就是一个过滤器链,现在来回顾一下这个整个过滤器链的请求和响应过程

image.png 主要就是这两个核心过滤器和一个拦截器

  • UsernamePasswordAuthenticationFilter:认证操作使用的过滤器
  • ExceptionTranslationFilter:异常转换的过滤器
  • FilterSecurityInterceptor:权限校验

SpringSecurity认证登录

数据库校验

1.准备工作

数据库表:自行参考或使用自己表结构

DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户id主键',
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话',
  `status` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '状态:0:停用,1:启用',
  `create_date` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `create_user` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建用户id',
  `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户昵称',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统用户信息表' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

java中相对应的实体,mapper,service,配置等可自行创建,就不一一添加了

2.核心代码

根据上文中的整个认证流程详解中可以看到我们需要对UserDetailsService接口,UserDetails接口实现重写,通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中

具体代码如下

UserDetailService实现类

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper<SysUser>().lambda()
                .eq(SysUser::getUsername, username));
        if (Objects.isNull(sysUser)) {
            throw new RuntimeException("用户名或密码不正确!");
        }
        // TODO 查询权限信息
       
        // 数据封装为UserDetails
        return new LoginUser(sysUser, permissions);
    }

}

UserDetails实现类

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser sysUser;

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // TODO 权限部分后续添加
        return null;
    }

    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

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

    @Override
    public boolean isAccountNonExpired() {
        // TODO 先置为true
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        // TODO 先置为true
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        // TODO 先置为true
        return true;
    }

    @Override
    public boolean isEnabled() {
        // TODO 先置为true
        return true;
    }
}

3.配置密码加密存储

添加配置类SecurityConfig

默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。一般不会采用这种方式。使用BCryptPasswordEncoder替换PasswordEncoder。

  • 使用SpringSecurity为我们提供的BCryptPasswordEncoder。

  • 把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

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

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

}

启动项目后访问测试跳转登录页面时使用数据库用户名密码即可

image.png

image.png

image.png

以上只是简单的将接口访问时需要登录认证的信息更改为数据库校验,下面添加完整登录流程,demo使用的为SpringSecurity+JWT,相应工具类可自行添加

登录接口完整流程

自定义登陆接口,让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

  • 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

  • 认证成功的话要生成一个jwt返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis(dome中未体现,可自行添加)

Controller

@RestController
@RequestMapping("/user")
public class LoginController {

    @Resource
    private LoginService loginService;

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String login(@RequestBody SysUser user) throws UnsupportedEncodingException {
        return loginService.login(user);
    }

}

LoginService

public interface LoginService {

    String login(SysUser user) throws UnsupportedEncodingException;

}

LoginServiceImpl

@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Override
    public String login(SysUser user) throws UnsupportedEncodingException {
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
        if (null == authenticate) {
            throw new RuntimeException("登录失败");
        }
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String token = JWTUtil.createToken(loginUser.getUsername());
        // TODO token存入redis(demo中未添加redis,可自行更改)
        return token;
    }


}

认证过滤器

@Component
public class JwtFilter extends OncePerRequestFilter {

    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasLength(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String username = JWTUtil.getUsername(token);
        // 从redis中读取用户信息 TODO 目前从数据库读取
        SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper<SysUser>().lambda()
                .eq(SysUser::getUsername, username));
                
        // TODO 权限部分后续添加

        LoginUser loginUser = new LoginUser(sysUser);
        // 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        filterChain.doFilter(request, response);
    }
}

配置类

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

    @Resource
    private JwtFilter jwtFilter;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭csrf
                .csrf().disable()
                // 不通过session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 登录接口允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面所有接口都需鉴权认证
                .anyRequest().authenticated();
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

运行结果

image.png

退出登录

demo中未接入redis,从SecurityContextHolder中获取认证信息,删除redis中对应的数据即可,不再演示

后续继续更新授权部分!!!

e7b0078e4cfc2435f0c424a5269d4d47.jpg