通常情况下,把API直接暴露出去是风险很大的。那么一般来说,对API要划分出一定的权限级别,然后做一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:
- 用户名和密码鉴权,使用Session保存用户鉴权结果。
- 使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
- 自行采用Token进行鉴权
这里主要讲一下JWT
JWT定义:
JWT是 Json Web Token
的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。
JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
JWT的工作流程
下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected
中)
- 用户导航到登录页,输入用户名、密码,进行登录
- 服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成JWT Token
- 服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
- 用户得到token,存在localStorage、cookie或其它数据存储形式中。
- 以后用户请求
/protected
中的API时,在请求的header中加入Authorization: Bearer xxxx(token)
。此处注意token之前有一个7字符长度的Bearer
- 服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
- 用户取得结果
添加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