Spring security + Jwt实现登录权限验证

Spring security + Jwt实现登录权限验证

Scroll Down

本案例是在上个案例的基础上改编的,由于是前后端分离,自动登录功能已去掉。功能不变,只是响应的页面改成api接口

环境搭建

springboot 版本2.4.1

导入额外的jwt坐标

本案例中没有使用thymeleaf相关jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.5.7</version>
</dependency>

Spring Data Jpa配置

和上个案例相同,其中还省去了自动登录的表

Spring security配置

jwt工具类

public class JJWTUtils {
    // 自定义设置签名 最少32位
//    private static final SecretKey key = Keys.hmacShaKeyFor("1234567890_1234567890_1234567890".getBytes());
    // 每次运行程序都会随机生成签名
    private static final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    public static String getToken(Map<String, Object> map) {
        Calendar c = Calendar.getInstance();
        // token有效期7天
        c.add(Calendar.DATE,7);

        String token = Jwts.builder()
                .setClaims(map)
                .setExpiration(c.getTime())
                .signWith(key).compact();
        return token;
    }

    public static Claims getClaims(String token) {
        Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
        return claims;
    }
}

自定义实现AuthenticationSuccessHandler接口

该接口用于处理登录成功时的操作,此时用来返回json数据

@Slf4j
@Component("authenticationSuccessHandler")
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @SneakyThrows
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("认证成功:【{}】",authentication.toString());
        HashMap<String, Object> map = new HashMap<>();
        map.put("username",authentication.getName());
        List<GrantedAuthority> authorities = (List<GrantedAuthority>) authentication.getAuthorities();
        List<String> list = authorities.stream().map(g -> g.getAuthority()).collect(Collectors.toList());
        map.put("role", list);
        String token = "Bearer "+ JJWTUtils.getToken(map);
        ServletUtil.setHeader(response, "Authorization", token);
        JSONObject obj = JSONUtil.createObj();
        obj.putOpt("code", 200);
        obj.putOpt("msg", "认证通过");
        obj.putOpt("Authorization", token);
        obj.putOpt("user", map);
        ServletUtil.write(response, obj.toString(),"application/json;charset=utf-8");
    }
}

自定义实现AuthenticationFailureHandler接口

该接口用于处理登录失败时的操作,例如返回用户名密码错误的json数据

@Slf4j
@Component("authenticationFailureHandler")
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        log.info("认证失败:【{}】",e.getMessage());
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", 500);
        if (e instanceof UsernameNotFoundException) {
            map.put("msg", e.getMessage());
        } else if (e instanceof BadCredentialsException) {
            map.put("msg", "密码错误"); //默认为:Bad credentials,友好提示
        }
        ServletUtil.write(httpServletResponse, JSONUtil.toJsonPrettyStr(map),"application/json;charset=utf-8");
    }
}

自定义实现AuthenticationEntryPoint接口

该接口用于处理没有登录时的操作,例如提示未登录的json数据

@Slf4j
@Component("authenticationEntryPoint")
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) {
        log.info("AuthenticationException:【{}】",e.toString());
        JSONObject jsonObject = JSONUtil.createObj();
        jsonObject.putOpt("code", 500);
        jsonObject.putOpt("msg", "请登陆!");
        ServletUtil.write(httpServletResponse,jsonObject.toString(),"application/json;charset=utf-8");
    }
}

自定义实现AccessDeniedHandler接口

该接口用来处理权限不足的操作

@Slf4j
@Component("accessDeniedHandler")
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        log.info("权限不足:【{}】",e.getMessage());
        JSONObject jsonObject = JSONUtil.createObj();
        jsonObject.putOpt("code", 403);
        jsonObject.putOpt("msg", "权限不足无法访问");
        ServletUtil.write(httpServletResponse,jsonObject.toString(),"application/json;charset=utf-8");
    }
}

自定义过滤器用于读取header中的token信息

并根据token不同的错误信息返回响应的json数据

@Slf4j
public class JwtVerifyFilter extends BasicAuthenticationFilter {
    private ObjectMapper objectMapper = new ObjectMapper();
    public JwtVerifyFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        // 从header获取token
        String header = request.getHeader("Authorization");
        if (Validator.isEmpty(header)) {
            // 如果获取不到就继续从请求参数中获取token
            header = request.getParameter("Authorization");
        }

        if (header == null || !header.startsWith("Bearer ")) {
            // 不论成功与否都要放行确保过滤器链不断
            chain.doFilter(request, response);
        } else {
            String token = header.replace("Bearer ", "");
            try {
                // 获取token中的用户名和角色信息
                Claims claims = JJWTUtils.getClaims(token);
                ArrayList<String> role = (ArrayList<String>) claims.get("role");
                log.info("role:{}", role);
                UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(claims.get("username"), null, AuthorityUtils.createAuthorityList(role.toArray(new String[role.size()])));
                SecurityContextHolder.getContext().setAuthentication(authResult);
                chain.doFilter(request, response);
            } catch (MalformedJwtException e) {
                sendMsg(response, "token无效");
            } catch (ExpiredJwtException e) {
                sendMsg(response, "toke过期");
            } catch (JwtException e) {
                sendMsg(response, "未知错误[" + e.toString() + "]");
            }
        }
    }

    @SneakyThrows
    public void sendMsg(HttpServletResponse response, String msg) {
        response.reset();
        HashMap<String, Object> map = new HashMap<>();
        map.put("code", 500);
        map.put("msg", msg);
        ServletUtil.write(response, objectMapper.writeValueAsString(map), "application/json;charset=utf-8");
    }
}

spring security配置类

把上面的自定义类都配置好

@Configuration
@Slf4j
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)  //开启security注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    // 认证失败的操作
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    // 认证成功的操作
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    // 为登录的操作
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    // 权限不足的操作
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    @Autowired
    private AuthenticationProvider authenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 自定义实现类,此时无论用户名不存在还是密码错误都提示密码错误
        // auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());

        auth.authenticationProvider(authenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf
        http.csrf().disable();

        // 登录成功和登录失败的操作
        http.formLogin().successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler);
        // 未登录和权限不足时的操作
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);

        // 除/hello外(测试),拦截所有路径
        http.authorizeRequests().antMatchers("/hello").permitAll();
        // 添加自定义的过滤器
        http.addFilter(new JwtVerifyFilter(authenticationManager()));
        // 关闭session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    /*
     * 密码加密器
     * */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


    /*
     * 登录的友好提示,为了区分用户名不存在和密码错误
     * */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 显示用户找不到异常
        provider.setHideUserNotFoundExceptions(false);
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userDetailsService);
        return provider;
    }

}

MVC配置

controller编写

和上一个案例没什么区别,就是修改一下返回的数据成了json形式

@RestController
@Slf4j
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }


    @GetMapping("/role/teacher")
    @Secured({"ROLE_teacher", "ROLE_admin"})
    public Object teacher() {
        Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        ArrayList<String> arrayList = new ArrayList<>();
        for (GrantedAuthority authority : authorities) {
            arrayList.add(authority.getAuthority());
        }
        HashMap<Object, Object> map = new HashMap<>();
        map.put("role", arrayList);
        map.put("msg", "教师界面");
        return map;
    }


    @GetMapping("/role/admin")
    @Secured({"ROLE_admin"})
    public Object admin() {
        Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        ArrayList<String> arrayList = new ArrayList<>();
        for (GrantedAuthority authority : authorities) {
            arrayList.add(authority.getAuthority());
        }
        HashMap<Object, Object> map = new HashMap<>();
        map.put("role", arrayList);
        map.put("msg", "管理员界面");
        return map;
    }


    @GetMapping("/role/student")
    @PreAuthorize("hasAnyRole('student','admin')")
    public Object student() {
        Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        ArrayList<String> arrayList = new ArrayList<>();
        for (GrantedAuthority authority : authorities) {
            arrayList.add(authority.getAuthority());
        }
        HashMap<Object, Object> map = new HashMap<>();
        map.put("role", arrayList);
        map.put("msg", "学生界面");
        return map;
    }
}