spring security实现权限功能

2022-09-29  本文已影响0人  sunpy

登录校验流程


spring security认证流程


  1. 浏览器用户提交用户名和密码。
  2. 将请求信息封装成Authentication,实现类为UsernamePasswordAuthenticationToken。
  3. authenticate()方法认证。
  4. AuthenticationManager委托认证authenticate()。
  5. DaoAuthenticationProvider通过loadUserByUsername方法获取用户信息。
  6. 返回UserDetails。
  7. 通过PasswordEncoder对比UserDetails中的密码与Authentication中密码是否一致。
  8. 填充Authentication对象,内容为权限信息。
  9. 返回给 UsernamePasswordAuthenticationFilter一个Authentication对象。
  10. 将Authentication对象放到SecurityContextHolder中。

部分代码需要重写实现业务


红色需要自己实现的逻辑

动手实现


  1. 实现自己数据库校验,重写UserDetailsService
@Service
public class LoginUserDetailsImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 重写loadUserByUsername方法
     * 查询自己数据库中用户信息
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StrUtil.isBlank(username)) {
            throw new CommonException("用户" + username + "不存在");
        }

        /**
         * 查询用户信息,认证
         */
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("user_name", username);
        User user = userMapper.selectOne(queryWrapper);

        if (Objects.isNull(user)) {
            throw new CommonException("该用户不存在");
        }

        UserBO userBO = new UserBO();
        BeanUtil.copyProperties(user, userBO);

        CustomUserDetailBO customUserDetailBO = new CustomUserDetailBO(userBO);

        return customUserDetailBO;
    }
}
  1. 实现自己登录服务接口
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    @PostMapping("/login")
    public ResultModel<String> login(@RequestBody UserBO userBO) throws CommonException {
        return userService.login(userBO);
    }
}

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public ResultModel<String> login(UserBO userBO) throws CommonException {
        /**
         * 1. 获取授权信息
         */
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(userBO.getUserName(), userBO.getPassword());
        Authentication authentication = authenticationManager.authenticate(token);

        if (Objects.isNull(authentication)) {
            throw new CommonException("登录失败");
        }

        /**
         * 获取授权信息中的用户信息
         */
        CustomUserDetailBO customUserDetailBO = (CustomUserDetailBO) authentication.getPrincipal();
        String userIdStr = customUserDetailBO.getUserBO().getUserId().toString();

        /**
         * 用户登录,认证成功,那么生成JWT,下次直接使用JWT访问
         */
        String jwt = JwtUtil.createJWT(userIdStr);

        /**
         * 将用户信息存储到redis
         */
        redisUtil.setCacheObject("token:"+ userIdStr, customUserDetailBO);

        ResultModel<String> resultModel = new ResultModel<>();
        resultModel.setMsg("登录成功");
        resultModel.setRes(jwt);
        return resultModel;
    }
}
  1. 配置spring security,过滤不需要的url
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 配置过滤的路径等
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(
                        SessionCreationPolicy.STATELESS
                )
                .and()
                .authorizeRequests()//csrf关闭
                .antMatchers("/user/**").permitAll()//放行
                .anyRequest().authenticated()//其他路径拦截
                .and()
                .formLogin().permitAll()//表单提交放行
        ;
    }

    /**
     * 注册解码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 暴露AuthenticationManager这个Bean
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager getAuthenticationManager() throws Exception {
        return super.authenticationManagerBean();
    }
}
  1. 测试

编写配置过滤器,拦截token


思路:过滤器在每一个请求到达Controller前,都会先经过Filter过滤,这时我们在过滤器里面获取其是否传递了token。
情况1:如果发现token正确,我们直接使用HttpServletResponse返回。
情况2:如果token不存在,那么我们交由后面的数据库查询检查。
情况3:如果redis中不存在token,那么说明redis中token过期或者logout登出删除了redis中的token,那么直接HttpServletResponse返回,提示其重新登录。

自定义过滤器:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, CommonException {
        // 获取token
        String token = request.getHeader("token");

        if (StrUtil.isBlank(token)) {
            filterChain.doFilter(request, response);
            return;
        }

        // 解析token
        String userId = (String) JwtUtil.parseJWT(token).get("userId");
        // 从redis获取用户信息
        CustomUserDetailBO customUserDetailBO = redisCache.getCacheObject("token:" + userId);
        ResultModel<String> resultModel = new ResultModel<>();
        resultModel.setTime(TimeUtil.getNowTime());

        if (Objects.isNull(customUserDetailBO)) {
            response.setStatus(500);
            resultModel.setCode(500);
            resultModel.setSuccess(false);
            resultModel.setMsg("用户未登陆");
            HttpUtil.setRespBody(response, resultModel);
            return;
        }

        // 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                customUserDetailBO, null, customUserDetailBO.getAuthorities()
        );

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        /*response.setStatus(200);
        resultModel.setMsg("用户登录成功");
        HttpUtil.setRespBody(response,resultModel);*/
        filterChain.doFilter(request, response);
    }
}

配置:

测试:

授权实现


UserDetailService查询数据库权限:

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StrUtil.isBlank(username)) {
            throw new CommonException("用户" + username + "不存在");
        }

        /**
         * TODO 查询用户信息,认证
         */
        QueryWrapper queryWrapper = new QueryWrapper();
        queryWrapper.eq("user_name", username);
        User user = userMapper.selectOne(queryWrapper);

        if (Objects.isNull(user)) {
            throw new CommonException("该用户不存在");
        }

        UserBO userBO = new UserBO();
        BeanUtil.copyProperties(user, userBO);

        /**
         * TODO 查询用户的权限信息,授权
         */
        List<Role> roleList = roleMapper.selectRolesByUserId(user.getUserId());

        List<RoleBO> roleBOList = roleList.stream().map(role -> {
            RoleBO roleBO = new RoleBO();
            BeanUtils.copyProperties(role, roleBO);
            List<Menu> menuList= menuMapper.selectMenuByRoleId(role.getRoleId());
            List<MenuBO> menuBOList = menuList.stream().map(menu -> {
                MenuBO menuBO = new MenuBO();
                BeanUtils.copyProperties(menu, menuBO);
                log.info("menuBO => {}", menuBO.getMenuCode());
                return menuBO;
            }).collect(Collectors.toList());
            roleBO.setMenuBOList(menuBOList);
            return roleBO;
        }).collect(Collectors.toList());

        return new CustomUserDetailBO(userBO, roleBOList);
    }

为url分配权限:

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        http.sessionManagement().sessionCreationPolicy(
                        SessionCreationPolicy.STATELESS
                )
                .and()
                .authorizeRequests()//csrf关闭
                .antMatchers("/user/r/r1").hasAuthority("01")
                .antMatchers("/user/r/r2").hasAuthority("02")
                .antMatchers("/user/r/r3").denyAll()
                .antMatchers("/user/**").permitAll()//放行
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll()//表单提交放行
                .and()
                .csrf().disable()
        ;
    }

    /**
     * 注册解码器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 暴露AuthenticationManager这个Bean
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager getAuthenticationManager() throws Exception {
        return super.authenticationManagerBean();
    }
}

编写CustomUserDetailBO类:

public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities != null){
            return authorities;
        }

        authorities = new ArrayList<>();
        //把permission中String类型的权限信息封装成SimpleGrantedAuthority对象
        roleBOList.stream().forEach(roleBO -> {
            roleBO.getMenuBOList().forEach(menuBO -> {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(menuBO.getMenuCode());
                authorities.add(authority);
            });
        });

        return authorities;
    }

过滤器修改:当用户登录成功,为其设置应该提供的权限:

// 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                customUserDetailBO, null, customUserDetailBO.getAuthorities()
        );

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

登出功能


思路:通过SecurityContextHolder类获取Authentication(里面封装了我们的登录信息),然后通过Authentication里面存储的User对象的用户id,然后拼接成redis的key获取redis中的对象,将其删除,这样下次拿着token来查询redis对象,发现没有了,就会提示其重新登录。

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private ILogoutService logoutService;

    @GetMapping("/logout")
    public ResultModel<String> logout() throws CommonException {
        return logoutService.logout();
    }
}

@Service
public class LogoutServiceImpl implements ILogoutService {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public ResultModel<String> logout() throws CommonException {
        UsernamePasswordAuthenticationToken authentication
                = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        CustomUserDetailBO customUserDetailBO = (CustomUserDetailBO) authentication.getPrincipal();
        Long userId = customUserDetailBO.getUserBO().getUserId();
        redisUtil.deleteObject("token:" + userId);

        ResultModel<String> resultModel = new ResultModel<>();
        resultModel.setTime(TimeUtil.getNowTime());
        resultModel.setMsg("用户注销成功");
        resultModel.setRes(String.valueOf(userId));
        return resultModel;
    }
}
上一篇下一篇

猜你喜欢

热点阅读