呕心4天出炉SpringBoot+security+JWT史上功能步骤最全的攻略

功能介绍:

为了模仿生产当中常用到的场景,我设置了3种情况:

1、首先使用security进行权限的管理,访问静态资源和获取token是不需要验证信息;

2、访问/admin/hello,/dba/hello,user/hello需要进行进行角色身份验证;

3、访问一般的链接“/info”只需要验证token信息;

流程介绍:

我们使用JWT来生成和验证token信息,加入到security到验证当中,然后系统会根据权限验证规则对用户对角色进行验证,如果验证成功跳转到相对应的角色页面,否则提示验证失败信息。

步入正题:

首先说一下要用到的依赖:


    org.springframework.boot
    spring-boot-starter-data-jpa


    org.springframework.boot
    spring-boot-starter-security
    2.1.6.RELEASE


    org.springframework.boot
    spring-boot-starter-jdbc


    org.springframework.boot
    spring-boot-starter-web


    org.projectlombok
    lombok
    true


    io.jsonwebtoken
    jjwt
    0.9.1

创建JWT的token生成验证工具类,这个类要结合Security的UserDetail信息进行token的生成和保存、验证功能

@Component
@Data
@Slf4j
public class JwtTokenConfig {

    //获取secret值
    @Value("${jwt.secret}")
    private String secret;

    //获取过期时间(单位毫秒)
    @Value("${jwt.expire}")
    private long expire;

    /**
     * 通过token验证是否有匹配的用户信息
     * @param token
     * @return
     */
    public String extractUsername(String token){
        return extractClaim(token,Claims::getSubject);
    }

    /**
     * 获取token是过期时间
     * @param token
     * @return
     */
    public Date extractExpiration(String token){
        return extractClaim(token,Claims::getExpiration);
    }

    /**
     * 查询该token保存的claims信息,如果没有返回null
     * @param token
     * @param claimsResolver
     * @param 
     * @return
     */
    public  T extractClaim(String token, Function claimsResolver){
        final Claims claims = getClaimByToken(token);
        return claimsResolver.apply(claims);
    }

    /**
     * 生成用户的token信息
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails){
        Map claims = new HashMap<>();
        return createToken(claims,userDetails.getUsername());
    }

    /**
     * 生成tokne的主方法
     * @param claims
     * @param username
     * @return
     */
    public String createToken(Map claims,String username){
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expire);
        return Jwts.builder().setClaims(claims)
//                .setHeaderParam("type","JWT")
                .setSubject(username).setIssuedAt(nowDate)
                .setExpiration(expireDate).signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * 获取与token匹配的claims,否则返回null
     * @param token
     * @return
     */
    public Claims getClaimByToken(String token){
        if(StringUtils.isEmpty(token)){
            return null;
        }
//        token = token.substring(7);
        System.out.println(token);
        try {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();

        }catch (Exception e){
            log.debug("验证token出错",e);
            return null;
        }
    }

    /**
     * 判断token是否过期
     * @param token
     * @return
     */
    public boolean isTokenExpired(String token){
        return extractExpiration(token).before(new Date());
    }

    /**
     * 验证token信息的用户信息是否相匹配
     * @param token
     * @param user
     * @return
     */
    public boolean validateToken(String token,UserDetails user){
        final String username = extractUsername(token);
        return username.equals(user.getUsername()) && !isTokenExpired(token);
    }
}

其次创建JWT的信息过滤器,目的是获取请求信息头header里面的token信息是否匹配,这里实现的是OncePerRequestFilter,该类是BasicAuthenticationFilter的父类,负责请求信息的过滤,详细使用请自行百度。

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    UserService userService;
    @Autowired
    JwtTokenConfig jwtTokenConfig;


    /**
     * 该方法进行请求头的信息过滤处理
     * 如果请求头的token不为null,就走jwt验证
     * 否则直接进行放行
     * @param request
     * @param response
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String username = null;
        String token = null;
        final String authenrication = request.getHeader("Authorization");
        if(authenrication != null && authenrication.startsWith("Bearer ")){
            token = authenrication.substring(7);
            //解析token获取username
            username = jwtTokenConfig.extractUsername(token);
            if(username !=null && SecurityContextHolder.getContext().getAuthentication() == null){
                UserDetails userDetails = userService.loadUserByUsername(username);
                //验证用户信息与token是否匹配
                if(jwtTokenConfig.validateToken(token,userDetails)){
                    //验证通过后主动生成security的权鉴token
                    UsernamePasswordAuthenticationToken utoken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    utoken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(utoken);
                }
            }
        }
        filterChain.doFilter(request,response);
    }
}

自定义Security的successHandler处理器类,实现AuthenticationSuccessHandler接口,重写AuthenticationSuccessHandler方法,该类的作用是当验证信息通过后,定义自己的流程处理,这里我们用它来实现不同的用户通过权限验证后跳转到自己对应的页面当中去。

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    protected Log logger = LogFactory.getLog(this.getClass());

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException{
        System.out.println("------------------------------");
        handler(request,response,authentication);
        clearAuthenticationAttributes(request);
    }


    protected void handler(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException{
        String targetUrl = determinTargetUrl(authentication);
        if(response.isCommitted()){
            logger.debug("response has already been commited .Unable to reredict to "+targetUrl);
        }
        redirectStrategy.sendRedirect(request,response,targetUrl);
    }

    protected String determinTargetUrl(final Authentication authentication) {
        //需要手动设置不同权限的角色跳转的路径是哪个
        Map roleUrlMap = new HashMap<>();
        roleUrlMap.put("ROLE_admin","/admin/hello");
        roleUrlMap.put("ROLE_dba","/dba/hello");
        roleUrlMap.put("ROLE_user","/user/hello");
        final Collection authorities = authentication.getAuthorities();
        for(final GrantedAuthority grantedAuthority:authorities){
            String authenriyName = grantedAuthority.getAuthority();
            //查询跳转路径里面有没有角色的信息,如果匹配上直接跳转到第一个配对的路径,否则报错处理
            if(roleUrlMap.containsKey(authenriyName)){
                return roleUrlMap.get(authenriyName);
            }
        }

        throw new IllegalStateException();
    }


    //清除session的权鉴信息
    protected void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

自定义FailurFilter身份校验失败处理器,实现AuthenticationFailureHandler,重写onAuthenticationFailure方法,提示错误信息:

@Component
public class MyAuthenticationFailurHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter out  = response.getWriter();
        response.setStatus(401);
        Map map = new HashMap<>();
        map.put("status",401);
        if(exception instanceof LockedException){
            map.put("msg","账户被锁定,登陆失败");
        }else if(exception instanceof BadCredentialsException){
            map.put("msg","账户或密码输入错误,登陆失败");
        }else if(exception instanceof DisabledException){
            map.put("msg","账户被禁用,登陆失败");
        }else  if(exception instanceof AccountExpiredException){
            map.put("msg","账户已过期,登陆失败");
        }else if(exception instanceof CredentialsExpiredException){
            map.put("msg","密码已过期,登陆失败");
        }else {
            map.put("msg","未知错误,登陆失败");
        }
        ObjectMapper om = new ObjectMapper();
        out.write(om.writeValueAsString(map));
        out.flush();
        out.close();
    }
}

以上我们基本实现了token的处理,权限校验成功后的处理,权限失败后的处理,接下来我们要进行一步比较重要的配置,自定义权限过滤器FilterInvocationSecurityMetadataSource,这个类的目的是当我们的successHandler处理完,匹配到了相应的role信息,然后我们需要通过该自定义类来生成相应的自定义权限Attribute信息,然后根据Attribute来决定走向。

@Component
public class CustomFIlterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    MenuRoleRepository menuRoleRepository;
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation)object).getRequestUrl();
        List menu_roles = menuRoleRepository.findAll();
        //如果是获取getken,设置生成该security的角色信息为role_token
        if(requestUrl.startsWith("/getToken")) {
            return SecurityConfig.createList("ROLE_TOKEN");
        }else
        {
            //查询数据库,根据菜单的url来和我们定义的路径匹配表达式进行正则匹配,如果匹配上了,把该角色设置到security的角色信息
            //否则设置成匿名角色
            for (MENU_ROLE menu : menu_roles) {
                MENU menu1 = menu.getMenu();
                if (antPathMatcher.match(menu1.getDurl(), requestUrl)) {
                    String[] roles = new String[1];
                    roles[0] = menu.getRole().getName();
                    System.out.println(roles[0]);
                    return SecurityConfig.createList(roles);
                }
            }
        }
        //其他都做登陆处理
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

与上面一起搭配起作用的是AccessDecisionManager,它是访问决策的管理者,如果角色信息过来后,是否准许它通过需要该管理者来决定,所以我们需要自定义一个管理者,来通过我们的“ROLE_TOKEN”等其他角色信息。

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    @Autowired
    RoleRepository roleRepository;

    @Override
    public void decide(Authentication authentication, Object object, Collection ca) throws AccessDeniedException, InsufficientAuthenticationException {
        Collection auths = authentication.getAuthorities();
        List roles = roleRepository.findAll();
        //进行角色信息的匹配,如果匹配上就放行,否则报权限不足,进行用户登陆
        for(ConfigAttribute configAttributes1:ca){
            if("ROLE_TOKEN".equals(configAttributes1.getAttribute())){
                return;
            }
            if("ROLE_LOGIN".equals(configAttributes1.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken){
                return;
            }
            for (Role role:roles){
                if(configAttributes1.getAttribute().equals(role.getName())){
                    return;
                }
            }
            for(GrantedAuthority authority:auths){
                if(configAttributes1.getAttribute().equals(authority.getAuthority())){
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

至此,我们的配置组件都写完了,该我们都securityConfig来登场,把他们都连接起来

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

    @Autowired
    UserService userService;
    @Autowired
    JwtRequestFilter jwtRequestFilter;
    @Autowired
    MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    MyAuthenticationFailurHandler myAuthenticationFailurHandler;
    @Autowired
    CustomFIlterInvocationSecurityMetadataSource customFIlterInvocationSecurityMetadataSource;
    @Autowired
    CustomAccessDecisionManager customAccessDecisionManager;

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception{
       http.cors()//支持跨域
       .and().csrf().disable()  //关闭csrf
       .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭session,使用totken

       .and().authorizeRequests()
       .withObjectPostProcessor(new ObjectPostProcessor() {

           @Override
           public  O postProcess(O object) {
                object.setAccessDecisionManager(customAccessDecisionManager);
                object.setSecurityMetadataSource(customFIlterInvocationSecurityMetadataSource);
                return object;
           }
       })
       .antMatchers("/","/getToken/**","/*.html","/css/**","/js**","/fonts/**",
            "/img/**","/static/**").permitAll()
       .antMatchers("/admin/hello").hasRole("admin")
       .antMatchers("/dba/hello").hasRole("dba")
       .antMatchers("/user/hello").hasRole("user")
       .anyRequest().authenticated()
        .and()
       .formLogin()
       .loginProcessingUrl("/login")
//               想使用sucesshandler必须把sessionmanager放开
       .successHandler(myAuthenticationSuccessHandler)
       .failureHandler(myAuthenticationFailurHandler)
       .and()
       .exceptionHandling().authenticationEntryPoint(new MyAutenticationEntryPoint())
       .and()
       .logout()
       .logoutUrl("/logout")
       .clearAuthentication(true)
       .addLogoutHandler(new MyLogoutHandler())
       .logoutSuccessHandler(new MyLogoutSuccessHandler())
       ;


       //在验证用户名和密码之前添加token过滤器,token是否有效
       http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }


    @Bean
    CorsConfigurationSource corsConfigurationSource(){
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }

}

测试用到的controller类

@RestController
public class SecurityController {


    @GetMapping("/admin/hello")
    public String adminhello(){
        return "admin hello";
    }

    @GetMapping("/dba/hello")
    public String dbahello(){
        return "dba hello";
    }
    @GetMapping("/user/hello")
    public String userhello(){
        return "user hello";
    }

    @GetMapping("/login_success")
    public String logoutSucc(){
        return "退出登陆成功";
    }
//
//    @PostMapping("/test")
//    public String test(){return "post mapping";}
}
@RestController
@Slf4j
public class TaskController {
    @Autowired
    private TaskMapper taskMapper;
    @Autowired
    UserService userService;
    @Autowired
    JwtTokenConfig jwtTokenConfig;

    @Autowired
    AuthenticationManager authenticationManager;

    /**
     * 通过查询username是否存在,来获取获取token
     * @param user
     * @return
     */
    @PostMapping("/getToken")
    public String getToken(@RequestBody User user){
        User u = (User) userService.loadUserByUsername(user.getUsername());
        if(u != null){
            String token = jwtTokenConfig.generateToken(u);
            System.out.println(token);
            return token;
        }
        return "用户名错误";
    }

    /**
     * 通过在header里面的autenrization传递token值
     * 来检查是否有权限访问信息
     * @param request
     * @return
     */
    @PostMapping("/info")
    public String getTaskInfo(HttpServletRequest request){
        String authHeader = request.getHeader("Authorization");
        log.info("查询任务表信息");

        List lst = taskMapper.findAll();
        System.out.println(lst.size());
        System.out.println(JSONArray.toJSONString(lst));
        return JSONArray.toJSONString(lst);
    }

}

配置类当中涉及到都一些实体类和数据库查询处理,都是非常简单的,就不贴上来了,需要了解的留言

案例测试

访问127.0.0.1:8081/getToken?username=xueqi

直接获取到token值

呕心4天出炉SpringBoot+security+JWT史上功能步骤最全的攻略_第1张图片

在不传递token的情况下访问/info,提示没有访问权限

呕心4天出炉SpringBoot+security+JWT史上功能步骤最全的攻略_第2张图片

然后在有token的情况下访问/info

呕心4天出炉SpringBoot+security+JWT史上功能步骤最全的攻略_第3张图片

访问127.0.0.1:8081/user/hello,先提示登陆操作,然后返回想对应的页面

呕心4天出炉SpringBoot+security+JWT史上功能步骤最全的攻略_第4张图片

你可能感兴趣的:(spring,springboot)