spring security配合jjwt完成权限管理02

本文详细介绍了如何使用Spring Security和JWT实现登录认证。首先讲解了AuthenticationProvider在用户认证中的作用,然后展示了自定义的CustomAuthenticationProvider类,用于处理登录逻辑。接着,介绍了UsernamePasswordAuthenticationFilter的自定义实现JWTLoginFilter,该过滤器处理用户登录请求并生成JWT token。最后,讨论了权限验证的实现,包括BasicAuthenticationFilter的子类JwtTokenInterceptor,用于解析和验证JWT token。文章还提到了权限不足时的处理类MyAccessDeniedHandler,并给出了完整的配置类SecurityConfig。

上一篇做好基础操作之后现在开始做最终开发,首先完成登录位置:

登录板块

首先关注AuthenticationProvider类,spring security用户认证主要是通过AuthenticationManager这个类完成的,但是但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider,所以我们要使用jwt配合security而又不通过security默认登录来做登录的时候,就需用户自己来完成认证工作了

import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;


public class CustomAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;

    private BCryptPasswordEncoder bCryptPasswordEncoder;
    public CustomAuthenticationProvider(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder){
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException,DisabledException {
        // 获取认证的用户名 & 密码
        String name = authentication.getName();
        String password = authentication.getCredentials().toString();
        // 认证逻辑
        UserDetails userDetails = userDetailsService.loadUserByUsername(name);
        //登录异常全部在JWTLoginFilter中统一抛出
        if (null != userDetails) {
            if (bCryptPasswordEncoder.matches(password, userDetails.getPassword())) {
                if (!userDetails.isAccountNonLocked()){
                    throw new DisabledException("当前账户被锁定了,请联系管理员");
                }else{
                    Authentication auth = new UsernamePasswordAuthenticationToken(name, password, userDetails.getAuthorities());
                    return auth;
                }

            } else {
                throw new BadCredentialsException("密码错误");
            }
        } else {
            throw new UsernameNotFoundException("用户不存在");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

这里主要是进行authenticate方法的重写,所有登录中的异常全部向上抛出,因为这里只是做认证动作,登录最终完成还是得去UsernamePasswordAuthenticationFilter这个类完成

首先UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter针对使用用户名和密码进行身份验证而定制化的一个过滤器
当用户登录得时候调用这个过滤器,底层自动调用AuthenticationProvider去认证可以配合完成登录认证工作



import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mysecurity.liuyf.pojo.User;
import com.mysecurity.liuyf.util.ResponseUtil;
import com.mysecurity.liuyf.util.ResultCode;
import com.mysecurity.liuyf.util.jwt.AuthException;
import com.mysecurity.liuyf.util.jwt.JwtUtil;

import com.mysecurity.liuyf.util.jwt.TokenException;
import lombok.SneakyThrows;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    private  JwtUtil jwtUtil;
    public JWTLoginFilter(AuthenticationManager authenticationManager,JwtUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil=jwtUtil;
    }

    // 尝试身份认证(接收并解析用户凭证)
    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
        Map<String, Object> data = new HashMap<>();
        try {
            User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
            Authentication authentication= authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            user.getUsername(),
                            user.getPassword(),
                            new ArrayList<>())
            );
            return authentication;
        } catch (IOException e) {
            System.out.println(e);
            throw new RuntimeException(e);
        }catch (Exception e) {
            //登录异常再这里捕获并且返回给前端数据
           // System.out.println(e.getMessage());
            data.put("code", ResultCode.LOGIN_FAIL.val());
            data.put("message", e.getMessage());
            ResponseUtil.sendMsg(res,data);
            return null;
        }
    }
    // 认证成功(用户成功登录后,这个方法会被调用,我们在这个方法里生成token)
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {

        Map<String, Object> data = new HashMap<>();
        // builder the token
        String token = null;
        try {
            //添加用户权限到token中去,动态修改权限的话不能立即生效
            Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
            // 定义存放角色权限集合的对象
            StringBuilder sb=new StringBuilder();

            Object[] grantes=authorities.toArray();
            for(int i=0;i<grantes.length;i++){
                sb.append(grantes[i]);
                if(i!=grantes.length-1){
                    sb.append(",");
                }
            }
           token= jwtUtil.generateToken(auth.getName()+"-"+sb.toString());
           // System.out.println(token);
            // 登录成功后,返回token到header里面
            data.put("code", ResultCode.SUCCESS.val());
            data.put("data", token);
            ResponseUtil.sendMsg(response,data);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里需要重写两个方法
attemptAuthentication方法尝试身份认证(接收并解析用户凭证)

successfulAuthentication这个方法,这个方法主要是登录认证成功并且加载了用户的资料后调用的,我们在这个位置使用jwtUtil.generateToken()创建了token(这里把权限也写进去了)
这样登录的整体就完成了,接收用户登录数据,调用security验证数据,最后把所需要的数据写到token中回传给前端(注意格式),让前端人员保存到客户端用于客户端访问的时候携带

权限认证板块

登录模块写好了,我们需要构造权限拦截、访问认证板块了
BasicAuthenticationFilter:主要做security的权限认证,我们需要编写类继承它



import com.mysecurity.liuyf.util.ResponseUtil;
import com.mysecurity.liuyf.util.ResultCode;
import com.mysecurity.liuyf.util.jwt.JwtUtil;
import com.mysecurity.liuyf.util.jwt.TokenException;
import io.jsonwebtoken.*;
import lombok.SneakyThrows;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

public class JwtTokenInterceptor extends BasicAuthenticationFilter {
    private  JwtUtil jwtUtil;
    public JwtTokenInterceptor(AuthenticationManager authenticationManager,JwtUtil jwtUtil) {
        super(authenticationManager);
        this.jwtUtil=jwtUtil;
    }

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, TokenException {
        Map<String, Object> data = new HashMap<>();
        String header = request.getHeader("token");
        if (header == null|| header.isEmpty()) {
            data.put("code", ResultCode.PARAMS_ERROR.val());
            data.put("message", ResultCode.PARAMS_ERROR.msg());
            ResponseUtil.sendMsg(response,data);
            return;
        }
        UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);
        if (authentication!=null){
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
        }
    }
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException {
        Map<String, Object> data = new HashMap<>();
        data.put("code", ResultCode.SYS_ERROR.val());
        String token = request.getHeader("token");
        // parse the token.
        String user = null;
        try {
            // 密钥
            Claims  claims = jwtUtil.parseToken(token);
           /* // token签发时间
            long issuedAt = claims.getIssuedAt().getTime();
            // 当前时间
            long currentTimeMillis = System.currentTimeMillis();
            // token过期时间
            long expirationTime = claims.getExpiration().getTime();*/
            //System.out.println(claims);
            user = claims.getSubject();
            if (user != null) {
                String[] split = user.split("-")[1].split(",");
                ArrayList<GrantedAuthority> authorities = new ArrayList<>();
                for (int i=0; i < split.length; i++) {
                   authorities.add(new SimpleGrantedAuthority(split[i]));
                }
                return new UsernamePasswordAuthenticationToken(user, null, authorities);
            }
       }
        /**
         * 捕获并返回前端token相关的异常
         */
        catch (ExpiredJwtException e) {
            data.put("message", "Token已经过期");
            ResponseUtil.sendMsg(response,data);
             return null;
        }
        catch (MalformedJwtException|UnsupportedJwtException|IllegalArgumentException|SignatureException e ) {
            data.put("message", "Token格式错误");
            ResponseUtil.sendMsg(response,data);
             return null;
        }
        catch (Exception e) {
            System.out.println(e);
            data.put("message", ResultCode.SYS_ERROR);
            ResponseUtil.sendMsg(response,data);
            return null;
        }
        return null;
    }
}

重写doFilterInternal方法去验证

UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);这个位置我们调用getAuthentication()方法,这这个方法中完成token的解析以及对当前token权限的解析(之前登录的时候随着用户名一起放置在jwt中,在这个位置反向解析出来,完成new UsernamePasswordAuthenticationToken()交给security去处理权限)

在getAuthentication方法中我们抛出token错误(乱写的token)和token过期的异常用于返回给前端

最后编写一个类继承自AccessDeniedHandler用于当用户没有某种权限的时候返回前端错误



import com.fasterxml.jackson.databind.ObjectMapper;
import com.mysecurity.liuyf.util.JsonResult;
import com.mysecurity.liuyf.util.ResultCode;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;


import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
            // TODO Auto-generated method stub
            response.setCharacterEncoding("UTF-8");
            response.setContentType("text/html;charset=utf-8");
        ObjectMapper mapper = new ObjectMapper();
        String result=null;
        result= mapper.writeValueAsString(new JsonResult(ResultCode.USER_NO_PERMISSION,ResultCode.USER_NO_PERMISSION.msg()));
        response.getWriter().print(result);
    }
}

这样我们基础都写完了,最后我们完成配置


@Configuration
//@EnableGlobalMethodSecurity(prePostEnabled=true)
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 权限不足
     * @return
     */
    @Bean
    AccessDeniedHandler customAccessDeniedHandler(){
        return new MyAccessDeniedHandler();
    }

 
  @Bean
  BCryptPasswordEncoder bCryptPasswordEncoder(){
      return new BCryptPasswordEncoder();
  }

    @Autowired
   private JwtUtil jwtUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 身份认证位置
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new CustomAuthenticationProvider(userDetailsService, bCryptPasswordEncoder()));
    }


    @Autowired
    ApplicationParams params;
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(params.getAntMatchers());
    }

   
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
               .authorizeRequests()
                .antMatchers("/userLogin").anonymous()
                .antMatchers(HttpMethod.GET,"/*.html").permitAll()
                .antMatchers("/sayHello").hasAnyAuthority("user:add")
                .antMatchers("/del").hasAnyAuthority("user:del")
                .antMatchers("/admin/say").hasRole("ADMIN")
                .antMatchers("/say").hasRole("SALE_ADMIN")
                .anyRequest().authenticated().and()
                .addFilter(new JWTLoginFilter(authenticationManager(),jwtUtil))
                .addFilter(new JwtTokenInterceptor(authenticationManager(),jwtUtil))
                .csrf().disable();
                //用于权限不足位置
        http.exceptionHandling().accessDeniedHandler(customAccessDeniedHandler());

    }
}

这里是做最后的配置,需要注意的是

.addFilter(new JWTLoginFilter(authenticationManager(),jwtUtil))
.addFilter(new JwtTokenInterceptor(authenticationManager(),jwtUtil))

这两个filter一个是加载登录位置,一个是权限认证位置

这样整体就写完了我们来测试一下,接下一篇

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值