Spring Security认证授权流程

1.认证授权过程

1.使用用户名和密码进行登录操作。

2.后端接受请求进入层层Filter过滤器,对登录操作放行,放行后开始认证流程。

3.Spring Security拿到用户名和密码,将其封装成一个实现了Authentication接口的UsernamePasswordAuthenticationToken。

4.使用认证管理器把上一步产生的TOKEN对象进行登录认证。也就是authenticationManager.authenticate(authenticationToken)

5.认证管理器AuthenticationManager认证

认证成功:返回一个封装了用户角色权限等信息的Authentication对象。

认证失败:抛出AuthenticationException异常,异常将被捕获交给AuthenticationEntryPoint 处理,处理后返回给前端页面引导重新登陆。

6.SecurityContextHolder.getContext().setAuthentication(…) ,把认证管理器返回的Authentication 对象赋予给当前的 SecurityContext上下文中。

7.认证成功后访问受保护的资源时,会使用保存在SecurityContext中的Authentication 对象进行相关的权限鉴定 ,若鉴定失败抛出AccessDeniedException,异常将被ExceptionTranslationFilter处理 ,然后返回状态码403表示没有权限。

2.举个栗子

配置项

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

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;


    //密码加密器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    //基于数据库认证&授权
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()  //关闭csrf
            .cors()  //解决跨域问题
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //前后端分离,禁用session
            .and()
             //请求认证配置
            .authorizeRequests()
            .mvcMatchers("/system/login")
            .permitAll()
            .anyRequest().authenticated();

        //认证授权过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter , UsernamePasswordAuthenticationFilter.class);

        //异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)  //401异常 认证失败
                .accessDeniedHandler(accessDeniedHandler);  //403异常  授权失败
    }

    //认证管理器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

        SpringSecurity默认是前后端不分离的方式,因此前后端分离项目需要禁言Session。然后排除登录的请求接口。由于在认证前需要验证Token,因此添加一个Token过滤器来进行Token验证。

        然后还需要指定401和403的失败情况的处理器,401异常进入AuthenticationEntryPoint进行处理,403鉴权失败异常进入AccessDeniedHandler进行处理,因此我们只需手动声明这两个Bean对象到IOC容器中,发生异常时就会自动进入到相应Bean中进行处理。

代码如下:


/**
 * 针对于Security的异常401 403统一做异常处理器
 */
@Configuration
public class SecurityHandler {

    //认证失败
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint(){
        return new AuthenticationEntryPoint() {
            @Override
            public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException){
//使用封装好的工具把字符串响应给客户端
                WebUtils.renderString(response , JSONUtil.toJsonStr(Result.error("401 认证失败,请先登陆")),401);
            }
        };
    }

    //授权失败
    @Bean
    public AccessDeniedHandler accessDeniedHandler(){
        return new AccessDeniedHandler() {
            @Override
            public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
                WebUtils.renderString(response, JSONUtil.toJsonStr(Result.error("403 授权失败,没有权限访问")),403);
            }
        };
    }
}

响应工具

/**
 * 将字符串渲染到客户端,向响应之中写入中聚类
 */
public class WebUtils {

    /**
     * 将字符串渲染到客户端
     * @param response 渲染对象
     * @param data   待渲染的字符串
     */
    public static String renderString(HttpServletResponse response, String data,Integer code) {
        try {
            response.setStatus(code);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    
}

UserDetails



/**
 * @description 存储worker的角色以及多个权限
 */

@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
    private Worker worker;

    public LoginUser(Worker worker) {
        this.worker = worker;
    }

    //已授予的权限
    private List<SimpleGrantedAuthority> authorities;

    //获取当前认证账户的角色和权限信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (!CollectionUtil.isEmpty(authorities)) {
            return authorities;
        }
        authorities = new ArrayList<>();
        Role role = worker.getRole();
        authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        Set<Permission> permissions = role.getPermissions();
        if(role.getPermissions()!=null){
            permissions.stream().forEach(permission -> {
                authorities.add(new SimpleGrantedAuthority(permission.getPermissionName()));
            });
        }
        return authorities;
}

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

    @Override
    public String getUsername() {
        return worker.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;
    }
}

        这里的Worker对象就是数据库中对应包含角色和权限信息的实体类,一个用户可以有多个角色,这里我直接就写了一个角色,一个角色对应多个权限。注意角色的唯一标识需要以ROLE_开头。下方可根据需求设置,这里统一返回true。这些判断会在认证过程中依次执行判断,若返回false则认证失败。

Token过滤器Filter

/**
 * * 解析请求头中的token,并验证合法性。
 * * 继承 OncePerRequestFilter 保证请求经过过滤器一次
 */
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtProperties jwtProperties;
    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String url = request.getRequestURI();
        if (url.equals("/system/login")) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }

        //1.从请求头中获取token数据
        String token = request.getHeader(jwtProperties.getTokenName());
        //2.验证token合法性
        log.info("验证token合法性:{}", token);
        Long id = null;
        try {
        Map<String, Object> map = JwtUtil.parseToken(jwtProperties.getSecretKey(), token);
        //直接object转Long会报错
            id = Long.parseLong(map.get("id").toString());
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Token已过期,请重新登陆");
        }
        //从redis中获取用户信息
        log.info("当前用户id:{}", id);
        ThreadLocalUtil.setCurrentId(id);
        Object worker = redisUtil.get(RedisKeyConstant.WORKER_PREFIX + "_" + id);
        if(ObjectUtil.isNull(worker)){
            throw new AccountNotFoundException("缓存中用户不存在,请重新登录");
        }
        Worker w = (Worker)worker;
        LoginUser loginUser = new LoginUser(w);

        //4.将Authentication对象(用户信息、已认证状态、权限信息)存入 SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser , null , loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

UserDetails登录业务

@Service
@Slf4j
public class SecurityUserDetailService implements UserDetailsService {

    @Autowired
    private WorkerMapper workerMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isEmpty(username)) {
            throw new RuntimeException("用户名不存在");
        }
        Worker worker = workerMapper.selectAll(phone);
        if (ObjectUtils.isEmpty(worker)) {
            throw new RuntimeException("账号不存在");
        }
        LoginUser loginUser = new LoginUser(worker);
        return loginUser;
    }
}

        这里是查询数据库获取用户信息的地方,通过Mapper编写SQL查询用户的角色和权限信息,用户和角色是一对多(我这里写的是一对一),角色和权限也是一对多。

用户登录业务逻辑

    public WorkerLoginVO login(String username,String password) {
        //密码登录
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
        Worker worker = loginUser.getWorker();
        //Redis缓存权限信息
        redisUtil.set(RedisKeyConstant.WORKER_PREFIX + "_" + worker.getId(), worker, 6 * 60 * 60L);
        //生成token,下面是自己的其他业务
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("id", worker.getId());
        String token = JwtUtil.genToken(jwtProperties.getSecretKey(), jwtProperties.getTtl(), claims);
        WorkerLoginVO workerLoginVO = WorkerLoginVO.builder().token(token).roleId(worker.getRoleId()).build();
        return workerLoginVO;
    }

认证授权的实现逻辑有多种方式,此处仅展示整个认证的过程,应根据自己业务需求来合理制定。

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值