SpringSecurity使用指南(一)自定义JWT认证流程

SpringSecurity是一个非常强大的安全验证框架,它通过一个过滤器链完成了对资源的访问控制。security框架本身提供了多种验证方式,如OA,Session等,但是有些情况下还是无法满足用户的需求,故此security框架也有非常大的扩展性,它允许用户自定义认证流程,下面我们就结合security实现一个jwt认证流程。

拦截登录请求

security框架中通过UsernamePasswordAuthenticationFilter来拦截登录请求,并将登录信息封装为待验证的token对象。由于我们需要自定义认证流程,所以要继承UsernamePasswordAuthenticationFilter的父类实现自己的拦截器来构造token。

public class JwtAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthenticationProcessingFilter() {
        //该过滤器会拦截哪些路径和类型的请求
        super(new AntPathRequestMatcher("/Jwtlogin", "POST"));
    }

    /**
     * 该方法是实现将用户登录信息从request提取,并封装成一个未认证的token传给AuthenticationManager处理
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @return
     * @throws AuthenticationException
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
        try {
            String username=httpServletRequest.getParameter("username");
            String password=httpServletRequest.getParameter("password");

            JwtToken jwtToken=new JwtToken(username,password);
            //将请求的会话id,IP地址保存在token中
            jwtToken.setDetails(new WebAuthenticationDetails(httpServletRequest));
            //调用认证管理器对token进行认证
            Authentication authentication=this.getAuthenticationManager().authenticate(jwtToken);
            return authentication;
        }catch (Exception e)
        {
            throw new BadCredentialsException("校验凭证失败,"+e.getMessage());
        }
    }
}

实现token

由于JwtAuthenticationProcessingFilter会调用AuthenticationManager进行认证,而AuthenticationManager内部存在多个认证方式,它通过token的类型来选择其中一种进行验证,因此我们需要实现一个自己的token。

public class JwtToken extends AbstractAuthenticationToken {

    private Object Principal;

    private Object Credential;

    /**
     * 构造等待认证的token
     * @param principal
     * @param credential
     */
    public JwtToken(Object principal, Object credential) {
        super(null);
        Principal = principal;
        Credential = credential;
        //设置token状态为未认证
        setAuthenticated(false);
    }

    /**
     * 构造已认证的token
     * @param authorities
     * @param principal
     * @param credential
     */
    public JwtToken(Collection<? extends GrantedAuthority> authorities, Object principal, Object credential) {
    	//保存权限
        super(authorities);
        Principal = principal;
        Credential = credential;
        //设置token状态为已认证
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.Credential;
    }

    @Override
    public Object getPrincipal() {
        return this.Principal;
    }
}

实现认证方式

Security的认证方式都实现AuthenticationProvider接口,所以我们只需要这个接口就可以自定义认证方式了,这个接口有authenticate和supports两个方法,authenticate接收一个待认证的token作为参数,认证成功则重新构造一个已认证的token并返回。supports方法用于判断是否该认证方式支持哪种token。

public class JwtAuthProvider implements AuthenticationProvider{

    @Resource(name = "UserDetailServiceImpl")
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtPasswordEncoder encoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtToken jwtToken= (JwtToken) authentication;
        UserDetails details=userDetailsService.loadUserByUsername(jwtToken.getName());
        AuthenticationCheck(details,jwtToken);
        SecurityCheck(details);
        JwtToken token=new JwtToken(details.getAuthorities(),details,jwtToken.getCredentials());
        return token;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return JwtToken.class.isAssignableFrom(aClass);
    }

    public void SecurityCheck(UserDetails details){
        if(!details.isAccountNonLocked()){
            throw new BadCredentialsException("The Account is locked");
        }
    }

    public void AuthenticationCheck(UserDetails details,JwtToken jwtToken){
        if(jwtToken.getCredentials()==null){
            throw new BadCredentialsException("The password not null");
        }
        if(!encoder.matches(details.getPassword(),jwtToken.getCredentials().toString())){
            throw new BadCredentialsException("The Password verification failed");
        }
    }
}

查询用户信息

在Security框架自己提供的认证方式中都是通过UserDetailsService接口的loadUserByUsername方法从数据库加载用户信息,因此这里我们入乡随俗实现该接口来加载用户信息

public class UserDetailServiceImpl implements UserDetailsService{
    @Autowired
    private UserService userServiceImp;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user=userServiceImp.getUserByAccount(s);
        if(user==null)throw new UsernameNotFoundException("用户账号:"+s+"不存在");
        Optional<String> op=userServiceImp.getUserAuthority(user.getUsername()).stream().findFirst();
        if(op.isPresent())return new SecurityUser(user,op.get());
        else return new SecurityUser(user);
    }
}

封装用户信息

public class SecurityUser implements UserDetails {

    private User user;

    private GrantedAuthority authorities;

    public SecurityUser(User user,String role) {
        this.user = user;
        this.authorities= new SimpleGrantedAuthority(role);
    }

    public SecurityUser(User user){this.user=user;}

    public SecurityUser() {
    }

    public SecurityUser(GrantedAuthority authoritie) {
        this.authorities = authorities;
    }

    public User getUser() {
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> list=new ArrayList<GrantedAuthority>();
        list.add(this.authorities);
        return list;
    }

    public void setAuthorities(String authorities) {
         this.authorities=new SimpleGrantedAuthority(authorities);
    }

    @Override
    @JSONField(serialize = false)
    public String getPassword() {
        return this.user.getUserpassword();
    }

    @Override
    @JSONField(serialize = false)
    public String getUsername() {
        return this.user.getUseraccount();
    }

    @Override
    @JSONField(serialize = false)
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    @JSONField(serialize = false)
    public boolean isAccountNonLocked() {
        return this.user.getStatus().equals("0");
    }

    @Override
    @JSONField(serialize = false)
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    @JSONField(serialize = false)
    public boolean isEnabled() {
        return false;
    }
}

访问控制过滤器

拦截所有请求,获取请求头的JWT token,如没有JWT token则直接放行,因为后续还有过滤器进行拦截,AnonymousAuthenticationFilter发现SecurityContextHolder中没有待验证的token时会加入一个已验证的匿名token,最终走到FilterSecurityInterceptor时,权限管理器再根据资源访问规则决定是否允许匿名用户访问资源。获取到JWT token并解密通过则构造已认证的token加入SecurityContextHolder。

public class JwtAccessFilter extends OncePerRequestFilter {


    public String getToken(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getHeader("Authorization");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token=this.getToken(httpServletRequest);

        if(token==null||token.isEmpty()){
            filterChain.doFilter(httpServletRequest,httpServletResponse);
            return;
        }

        SecurityUser securityUser;
        try {
            String jsonUser=JwtUtil.parseToken(token).get("data");
            securityUser=JSONObject.parseObject(jsonUser,SecurityUser.class);
            JSONObject jsonObject=JSONObject.parseObject(jsonUser);
            securityUser.setAuthorities(jsonObject.getJSONArray("authorities").getJSONObject(0).getString("authority"));

        }catch (Exception e){
            e.printStackTrace();
            httpServletResponse.setContentType("application/json;charset=UTF-8");
            httpServletResponse.getWriter().print(JSONObject.toJSONString(new AjaxResponseBody("401","token已失效,请重新登录",null)));
            return;
        }
        JwtToken jwtToken=new JwtToken(securityUser.getAuthorities(),securityUser,"");
        jwtToken.setDetails(new WebAuthenticationDetails(httpServletRequest));

        SecurityContextHolder.getContext().setAuthentication(jwtToken);
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

资源访问规则

@Component("AccessRuleService")
public class AccessRuleService {

    private AntPathMatcher antPathMatcher=new AntPathMatcher();

    public boolean controll(HttpServletRequest request, Authentication authentication){
        String url=request.getRequestURI();

        if(antPathMatcher.match("/anyone",url)) return true;

        if(authentication instanceof AnonymousAuthenticationToken)return false;

        SecurityUser securityUser= (SecurityUser) authentication.getPrincipal();
        Optional<String> op=securityUser.getAuthorities().stream().map(i->i.getAuthority()).findFirst();
        if(!op.isPresent())return false;
        List<String> Urls=queryUrlByAuthorities(op.get());
        for (String l:Urls) {
            if(antPathMatcher.match(l,url))return true;
        }

        return false;
    }

    public List<String> queryUrlByAuthorities(String authoritie){
            switch (authoritie){

                case "USER":{
                    return Arrays.asList("/user");
                }

                case "ADMIN":{return Arrays.asList("/user","/admin");}

                case "SUPER ADMIN":{return Arrays.asList("/user","/admin","/super");}

                default:return null;
            }
    }
}

登录成功处理器

public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        try {
            AjaxResponseBody ajaxResponseBody = null;
            SecurityUser securityUser = (SecurityUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            httpServletResponse.setHeader("Content-Type", "application/json;charset=utf-8");

            JSONObject json=new JSONObject();
            JSONObject userjson= (JSONObject) JSONObject.toJSON(securityUser.getUser());
            json.put("token",JwtUtil.createJwtToken(JSONObject.toJSONString(securityUser)));
            userjson.remove("userpassword");
            json.put("userinfo",userjson);
            ajaxResponseBody = new AjaxResponseBody("200", "success", json);
            httpServletResponse.getWriter().write(JSONObject.toJSONString(ajaxResponseBody));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

失效token

由于token的无状态特点,因此我们很难自行失效token。笔者感觉非要失效token的话,在网上找到的诸多方法中,黑名单机制或许是一种比较好的方式。将需要失效的token存入数据库,每次访问时查看token存在失效表中。这里就由读者自行实现吧,笔者懒得弄了。

组装配置

@SpringBootConfiguration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationFailHandler myAuthenticationFailHandler;
    @Autowired
    private JwtAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private JwtLogoutSuccessHandler myLogoutSuccessHandler;
    @Autowired
    private JwtLogoutHandler myLogoutHandler;
    @Autowired
    private JwtAccessFilter jwtAccessFilter;
    @Autowired
    private JwtAuthProvider provider;

    @Bean(name = "UserDetailServiceImpl")
    public UserDetailsService customUserService() {
        return new UserDetailServiceImpl();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义UserDetailsService
        auth.userDetailsService(customUserService()).passwordEncoder(new JwtPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        JwtAuthenticationProcessingFilter processingFilter=new JwtAuthenticationProcessingFilter();
        //设置认证失败后的处理器
        processingFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
        //设置认证成功的处理器
        processingFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        //设置用于认证的认证管理器
        processingFilter.setAuthenticationManager(this.authenticationManagerBean());
		//将认证流程过滤器放到security的过滤器链中
        http.addFilterAfter(processingFilter, UsernamePasswordAuthenticationFilter.class)
        //将自定义的认证方式加入过认证管理器
                .authenticationProvider(provider)
                //token拦截器加入过滤器链中
                .addFilterAfter(jwtAccessFilter,JwtAuthenticationProcessingFilter.class)
                .exceptionHandling()
                //设置访问权限不足时的处理器访问
                .accessDeniedHandler(new JwtAccessDeniedHandler())
                //设置匿名用户访问被拒绝时的处理器
                .authenticationEntryPoint(new JwtAuthenticationEntryPoint())
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().csrf().disable();
		//配置登出处理器
        http.logout().addLogoutHandler(myLogoutHandler).logoutSuccessHandler(myLogoutSuccessHandler);
		//配置资源访问规则
        http.authorizeRequests().anyRequest().access("@AccessRuleService.controll(request,authentication)");

    }
}

测试

SpringSecurity使用指南(一)自定义JWT认证流程_第1张图片

SpringSecurity使用指南(一)自定义JWT认证流程_第2张图片

SpringSecurity使用指南(一)自定义JWT认证流程_第3张图片

你可能感兴趣的:(Spring,Security)