通常情況下,把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