SpringSecurity
SpringSecurity是一個強大的可高度定制的認證和授權框架,對於Spring應用來說它是一套Web安全標准;
JWT
JWT是JSON WEB TOKEN的縮寫,它是基於 RFC 7519 標准定義的一種可以安全傳輸的的JSON對象,由於使用了數字簽名,所以是可信任和安全的。
JWT的應用場景:
身份認證在這種場景下,一旦用戶完成了登陸,在接下來的每個請求中包含JWT,可以用來驗證用戶身份以及對路由,服務和資源的訪問權限進行驗證。由於它的開銷非常小,可以輕松的在不同域名的系統中傳遞,所有目前在單點登錄(SSO)中比較廣泛的使用了該技術
JWT的組成
JWT token的格式:header.payload.signature
header中用於存放簽名的生成算法
payload中用於存放用戶名、token的生成時間和過期時間
signature為以header和payload生成的簽名,
一旦header和payload被篡改,驗證將失敗
JWT實現認證和授權的原理:
- 用戶調用登錄接口,登錄成功后獲取到JWT的token;
- 之后用戶每次調用接口都在http的header中添加一個叫Authorization的頭,值為JWT的token;
- 后台程序通過對Authorization頭中信息的解碼及數字簽名校驗來獲取其中的用戶信息,從而實現認證和授權。
特點:token保存在客戶端,而不是服務端!!!
項目表的介紹:
ums_admin:用戶表ums_role:角色表ums_permission:權限表ums_admin_role_relation:用戶角色表ums_role_permission_relation:角色權限表ums_admin_permission_relation:用戶權限表
介紹完springSecurity和JWT,下面來整合他們;
首先在pom.xml添加相關依賴:
<!--SpringSecurity依賴配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Hutool Java工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.7</version>
</dependency>
<!--JWT(Json Web Token)登錄支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
添加JWT token的工具類,這里介紹下工具類等的方法說明
- generateToken(UserDetails userDetails) :用於根據登錄用戶信息生成token
- getUserNameFromToken(String token):從token中獲取登錄用戶的信息
- validateToken(String token, UserDetails userDetails):判斷token是否還有效
package com.macro.mall.tiny.common.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * JwtToken生成的工具類 * Created by macro on 2018/4/26. */ @Component public class JwtTokenUtil { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; /** * 根據負責生成JWT的token */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 從token中獲取JWT中的負載 */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { LOGGER.info("JWT格式驗證失敗:{}",token); } return claims; } /** * 生成token的過期時間 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } /** * 從token中獲取登錄用戶名 */ public String getUserNameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 驗證token是否還有效 * * @param token 客戶端傳入的token * @param userDetails 從數據庫中查詢出來的用戶信息 */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判斷token是否已經失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } /** * 從token中獲取過期時間 */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 根據用戶信息生成token */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 判斷token是否可以被刷新 */ public boolean canRefresh(String token) { return !isTokenExpired(token); } /** * 刷新token */ public String refreshToken(String token) { Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } }
添加SpringSecurity的配置類
import com.macro.mall.tiny.component.JwtAuthenticationTokenFilter; import com.macro.mall.tiny.component.RestAuthenticationEntryPoint; import com.macro.mall.tiny.component.RestfulAccessDeniedHandler; import com.macro.mall.tiny.dto.AdminUserDetails; import com.macro.mall.tiny.mbg.model.UmsAdmin; import com.macro.mall.tiny.mbg.model.UmsPermission; import com.macro.mall.tiny.service.UmsAdminService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import java.util.List; /** * SpringSecurity的配置 * Created by macro on 2018/4/26. */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UmsAdminService adminService; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Autowired private RestAuthenticationEntryPoint restAuthenticationEntryPoint; @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf()// 由於使用的是JWT,我們這里不需要csrf .disable() .sessionManagement()// 基於token,所以不需要session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(HttpMethod.GET, // 允許對於網站靜態資源的無授權訪問 "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-resources/**", "/v2/api-docs/**" ) .permitAll() .antMatchers("/admin/login", "/admin/register")// 對登錄注冊要允許匿名訪問 .permitAll() .antMatchers(HttpMethod.OPTIONS)//跨域請求會先進行一次options請求 .permitAll() // .antMatchers("/**")//測試時全部運行訪問 // .permitAll() .anyRequest()// 除上面外的所有請求全部需要鑒權認證 .authenticated(); // 禁用緩存 httpSecurity.headers().cacheControl(); // 添加JWT filter httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //添加自定義未授權和未登錄結果返回 httpSecurity.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthenticationEntryPoint); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService() { //獲取登錄用戶信息 return username -> { UmsAdmin admin = adminService.getAdminByUsername(username); if (admin != null) { List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId()); return new AdminUserDetails(admin,permissionList); } throw new UsernameNotFoundException("用戶名或密碼錯誤"); }; } @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){ return new JwtAuthenticationTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
相關依賴及方法說明
- configure(HttpSecurity httpSecurity):用於配置需要攔截的url路徑、jwt過濾器及出異常后的處理器;
- configure(AuthenticationManagerBuilder auth):用於配置UserDetailsService及PasswordEncoder;
- RestfulAccessDeniedHandler:當用戶沒有訪問權限時的處理器,用於返回JSON格式的處理結果;
- RestAuthenticationEntryPoint:當未登錄或token失效時,返回JSON格式的結果;
- UserDetailsService:SpringSecurity定義的核心接口,用於根據用戶名獲取用戶信息,需要自行實現;
- UserDetails:SpringSecurity定義用於封裝用戶信息的類(主要是用戶信息和權限),需要自行實現;
- PasswordEncoder:SpringSecurity定義的用於對密碼進行編碼及比對的接口,目前使用的是BCryptPasswordEncoder;
- JwtAuthenticationTokenFilter:在用戶名和密碼校驗前添加的過濾器,如果有jwt的token,會自行根據token信息進行登錄。
添加RestfulAccessDeniedHandler
import cn.hutool.json.JSONUtil; import com.macro.mall.tiny.common.api.CommonResult; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 當訪問接口沒有權限時,自定義的返回結果 * Created by macro on 2018/4/26. */ @Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler{ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage()))); response.getWriter().flush(); } }
添加RestAuthenticationEntryPoint
import cn.hutool.json.JSONUtil; import com.macro.mall.tiny.common.api.CommonResult; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * 當未登錄或者token失效訪問接口時,自定義的返回結果 * Created by macro on 2018/5/14. */ @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage()))); response.getWriter().flush(); } }
添加AdminUserDetails
用戶類實現UserDetails接口,
import com.macro.mall.tiny.mbg.model.UmsAdmin; import com.macro.mall.tiny.mbg.model.UmsPermission; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * SpringSecurity需要的用戶詳情 * Created by macro on 2018/4/26. */ public class AdminUserDetails implements UserDetails { private UmsAdmin umsAdmin; private List<UmsPermission> permissionList; public AdminUserDetails(UmsAdmin umsAdmin, List<UmsPermission> permissionList) { this.umsAdmin = umsAdmin; this.permissionList = permissionList; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { //返回當前用戶的權限 return permissionList.stream() .filter(permission -> permission.getValue()!=null) .map(permission ->new SimpleGrantedAuthority(permission.getValue())) .collect(Collectors.toList()); } @Override public String getPassword() { return umsAdmin.getPassword(); } @Override public String getUsername() { return umsAdmin.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return umsAdmin.getStatus().equals(1); } }
添加JwtAuthenticationTokenFilter
在用戶名和密碼校驗前添加的過濾器,如果請求中有jwt的token且有效,會取出token中的用戶名,然后調用SpringSecurity的API進行登錄操作。
import com.macro.mall.tiny.common.utils.JwtTokenUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * JWT登錄授權過濾器 * Created by macro on 2018/4/26. */ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(this.tokenHead)) { String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer " String username = jwtTokenUtil.getUserNameFromToken(authToken); LOGGER.info("checking username:{}", username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); LOGGER.info("authenticated user:{}", username); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
