Spring Boot+JWT+Spring Security实现授权认证保护Rest API

 

 

 

 

 

通常情况下,把API直接暴露出去是风险很大的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:

  1. 用户名和密码鉴权,使用Session保存用户鉴权结果。
  2. 使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
  3. 自行采用Token进行鉴权

这里主要讲一下JWT

JWT定义:

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。

JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

JWT的工作流程

下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected中)

  1. 用户导航到登录页,输入用户名、密码,进行登录
  2. 服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成JWT Token
  3. 服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
  4. 用户得到token,存在localStorage、cookie或其它数据存储形式中。
  5. 以后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
  6. 服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
  7. 用户取得结果

 

添加maven依赖:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
<dependencies>
    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter—web</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-thymeleaf</artifactId>
        </dependency>
<dependencies>

 JWT生成的代码:

static public String createAccessToken(Authentication auth) {
        AccountCredentials credentials = (AccountCredentials) auth.getDetails();
        String userName = auth.getName();
        String role = credentials.isAdmin() ? "ROLE_ADMIN" : "ROLE_USER";
        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE, TOKENEXPIRATIONTIME);
        String accessToken = Jwts.builder().claim("authorities", role).claim("username", userName)
                .claim("userid", credentials.getUserId()).setSubject(userName).setIssuer(TOKENISSUER)
                .setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(nowTime.getTime())
                .signWith(SignatureAlgorithm.HS512, SECRET).compact();
        return accessToken;
    }

Security:

入口过滤器

@Configuration
@EnableWebSecurity
@Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    @Value("${server.context-path}")
    String contextPath;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.headers().xssProtection().xssProtectionEnabled(true);
        http.csrf().disable().exceptionHandling().authenticationEntryPoint(myEntryPoint())
        .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and().authorizeRequests()
        .antMatchers(HttpMethod.OPTIONS).permitAll().antMatchers("/").permitAll().antMatchers("/images/**")
        .permitAll().antMatchers("/*.html").permitAll().antMatchers("/hello/**").permitAll()
        .antMatchers("/admin/add").permitAll().antMatchers("/admin/encode").permitAll()
        .antMatchers("/admin/delete").permitAll().antMatchers("/category/list").permitAll()
        .antMatchers("/module/list").permitAll().antMatchers("/photo/list").permitAll()
        .antMatchers("/music/list").permitAll().antMatchers("/doc/list").permitAll().antMatchers("/vr/list")
        .permitAll().antMatchers("/video/list").permitAll().antMatchers("/category/list/privateOpen")
        .permitAll().antMatchers("/photo/list/privateOpen").permitAll().antMatchers("/video/list/privateOpen")
        .permitAll().antMatchers("/music/list/privateOpen").permitAll().antMatchers("/doc/list/privateOpen")
        .permitAll().antMatchers("/vr/list/privateOpen").permitAll().antMatchers("/health/**").permitAll()
        .antMatchers("/favicon.ico").permitAll().antMatchers("**/*.html").permitAll().antMatchers("**/*.css")
        .permitAll().antMatchers("**/*.js").permitAll().antMatchers("/", "/*swagger*/**", "/v2/api-docs")
        .permitAll()
        .antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security",
                "/swagger-ui.html", "/webjars/**")
        .permitAll()
        // .antMatchers(HttpMethod.POST, "/logout").authenticated()
        // 所有 /login 的POST请求 都放行
        .antMatchers(HttpMethod.POST, "/login/**").permitAll()

        .antMatchers(HttpMethod.GET, "/token/**").permitAll()
        // 所有请求需要身份认证
        .anyRequest().authenticated().and()
        // 添加一个过滤器 所有访问 /login 的请求交给 JWTLoginFilter 来处理 这个类处理所有的JWT相关内容
        .addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
                UsernamePasswordAuthenticationFilter.class)
        // 添加一个过滤器验证其他请求的Token是否合法
        .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new CustomAuthenticationProvider());
    }
    @Bean
    AuthenticationEntryPoint myEntryPoint() {
        return new ExampleAuthenticationEntryPoint();
    }

}

JWT认证登录、鉴权

 

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse res)
            throws AuthenticationException, IOException, ServletException {
        try {
            AccountCredentials creds = new AccountCredentials();
            if (creds == null || StringUtils.isEmpty(creds.getUsername()) || StringUtils.isEmpty(creds.getPassword())) {
                String result = JSONResult.fillResultString(HttpServletResponse.SC_BAD_REQUEST, "请求参数无效", null);
                res.setContentType("application/json;charset=UTF-8");
                res.getWriter().println(result);
            }
            return getAuthenticationManager()
                    .authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword()));
        } catch (JsonMappingException ex) {
            String result = JSONResult.fillResultString(1, "[参数异常]", null);
            res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            res.getWriter().println(result);
            res.getWriter().close();
        } catch (Exception e) {
            e.printStackTrace();
            String result = JSONResult.fillResultString(10006, "鉴权失败,请重新登录", null);
            res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            res.getWriter().println(result);
            res.getWriter().close();
        }
        return null;
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
            Authentication auth) throws IOException, ServletException {
        TokenAuthenticationService.addAuthentication(res, auth);//响应返回JWT Token
        clearAuthenticationAttributes(req);

    }
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res,
            AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        res.setContentType("application/json;charset=UTF-8");
        res.setStatus(HttpServletResponse.SC_OK);
        String result = JSONResult.fillResultString(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error!!", null);
        res.getWriter().println(result);
        res.getWriter().close();
    }
    protected final void clearAuthenticationAttributes(HttpServletRequest req) {
        HttpSession session = req.getSession(false);
        if(session == null){
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

认证用户名和密码:

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取认证的用户名 & 密码
        String userName = authentication.getName();
        String passWord = authentication.getCredentials().toString();
        if (loginService == null) {
            loginService = SpringContextUtil.getBean("loginService");
        }
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
        String appId = request.getHeader("appId");
        AccountCredentials user = loginService.login(userName, passWord,appId);
        // 认证逻辑
        if (user != null) {

            // // 这里设置权限和角色
            // ArrayList<GrantedAuthority> authorities = new ArrayList<>();
            // authorities.add( new GrantedAuthorityImpl("ROLE_ADMIN") );
            // // authorities.add( new GrantedAuthorityImpl("AUTH_WRITE") );
            // // 生成令牌
            // Authentication auth = new UsernamePasswordAuthenticationToken(name, password,
            // authorities);
            // 生成令牌
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userName, passWord);
            auth.setDetails(user);
            return auth;
        } else {
             throw new BadCredentialsException("密码错误~");
        }
    }

demo源码下载链接:https://pan.baidu.com/s/1kWYeBUR,密码:plr4

 

posted on 2018-02-11 16:52  依米欧  阅读(1636)  评论(0编辑  收藏  举报