用戶認證流程以下:前端
業務流程說明以下:web
一、客戶端請求認證服務進行認證。
二、認證服務認證經過向瀏覽器cookie寫入token(身份令牌)
認證服務請求用戶中心查詢用戶信息。
認證服務請求Spring Security申請令牌。
認證服務將token(身份令牌)和jwt令牌存儲至redis中。
認證服務向cookie寫入 token(身份令牌)。
三、前端攜帶token請求認證服務獲取jwt令牌
前端獲取到jwt令牌並存儲在sessionStorage。
前端從jwt令牌中解析中用戶信息並顯示在頁面。
四、前端攜帶cookie中的token身份令牌及jwt令牌訪問資源服務
前端請求資源服務須要攜帶兩個token,一個是cookie中的身份令牌,一個是http header中的jwt令牌
前端請求資源服務前在http header上添加jwt請求資源
五、網關校驗token的合法性redis
用戶請求必須攜帶 token身份令牌和jwt令牌
網關校驗redis中token是否合法,已過時則要求用戶從新登陸
六、資源服務校驗jwt的合法性並完成受權
資源服務校驗jwt令牌,完成受權,擁有權限的方法正常執行,沒有權限的方法將拒絕訪問。spring
用戶中心對外提供以下接口:
一、響應數據類型數據庫
此接口未來被用來查詢用戶信息及用戶權限信息,因此這裏定義擴展類型json
@Data @ToString public class XcUserExt extends XcUser { //權限信息 private List<XcMenu> permissions; //企業信息 private String companyId; }
二、根據帳號查詢用戶信息api
@Api(value = "用戶中心",description = "用戶中心管理") public interface UcenterControllerApi { public XcUserExt getUserext(String username); }
添加XcUser、XcCompantUser兩個表的Dao瀏覽器
public interface XcUserRepository extends JpaRepository<XcUser, String> { XcUser findXcUserByUsername(String username); } public interface XcCompanyUserRepository extends JpaRepository<XcCompanyUser,String> { //根據用戶id查詢所屬企業id XcCompanyUser findByUserId(String userId); }
@Service public class UserService { @Autowired private XcUserRepository xcUserRepository; //根據用戶帳號查詢用戶信息 public XcUser findXcUserByUsername(String username){ return xcUserRepository.findXcUserByUsername(username); } //根據帳號查詢用戶的信息,返回用戶擴展信息 public XcUserExt getUserExt(String username){ XcUser xcUser = this.findXcUserByUsername(username); if(xcUser == null){ return null; } XcUserExt xcUserExt = new XcUserExt(); BeanUtils.copyProperties(xcUser,xcUserExt); //用戶id String userId = xcUserExt.getId(); //查詢用戶所屬公司 XcCompanyUser xcCompanyUser = xcCompanyUserRepository.findXcCompanyUserByUserId(userId); if(xcCompanyUser!=null){ String companyId = xcCompanyUser.getCompanyId(); xcUserExt.setCompanyId(companyId); } return xcUserExt; } }
@RestController @RequestMapping("/ucenter") public class UcenterController implements UcenterControllerApi { @Autowired UserService userService; @Override @GetMapping("/getuserext") public XcUserExt getUserext(@RequestParam("username") String username) { XcUserExt xcUser = userService.getUserExt(username); return xcUser; } }
認證服務須要遠程調用用戶中心服務查詢用戶,在認證服務中建立Feign客戶端安全
@FeignClient(value = XcServiceList.XC_SERVICE_UCENTER) public interface UserClient { @GetMapping("/ucenter/getuserext") public XcUserExt getUserext(@RequestParam("username") String username) }
認證服務調用spring security接口申請令牌,spring security接口會調用UserDetailsServiceImpl從數據庫查詢用
戶,若是查詢不到則返回 NULL,表示不存在;在UserDetailsServiceImpl中將正確的密碼返回, spring security
會自動去比對輸入密碼的正確性。cookie
一、修改UserDetailsServiceImpl的loadUserByUsername方法,調用Ucenter服務的查詢用戶接口
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserClient userClient; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //取出身份,若是身份爲空說明沒有認證 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //沒有認證統一採用httpbasic認證,httpbasic中存儲了client_id和client_secret,開始認證 client_id和client_secret if(authentication==null){ ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username); if(clientDetails!=null){ //密碼 String clientSecret = clientDetails.getClientSecret(); return new User(username,clientSecret,AuthorityUtils.commaSeparatedStringToAuthorityList("")); } } if (StringUtils.isEmpty(username)) { return null; } //請求ucenter查詢用戶 XcUserExt userext = userClient.getUserext(username); if(userext == null){ //返回NULL表示用戶不存在,Spring Security會拋出異常 return null; } //從數據庫查詢用戶正確的密碼,Spring Security會去比對輸入密碼的正確性 String password = userext.getPassword(); String user_permission_string = ""; UserJwt userDetails = new UserJwt(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string)); //用戶id userDetails.setId(userext.getId()); //用戶名稱 userDetails.setName(userext.getName()); //用戶頭像 userDetails.setUserpic(userext.getUserpic()); //用戶所屬企業id userDetails.setCompanyId(userext.getCompanyId()); return userDetails; } }
早期使用md5對密碼進行編碼,每次算出的md5值都同樣,這樣很是不安全,Spring Security推薦使用
BCryptPasswordEncoder對密碼加隨機鹽,每次的Hash值都不同,安全性高。
一、BCryptPasswordEncoder測試程序以下
@Test public void testPasswrodEncoder(){ String password = "111111"; PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); for(int i=0;i<10;i++) { //每一個計算出的Hash值都不同 String hashPass = passwordEncoder.encode(password); System.out.println(hashPass); //雖然每次計算的密碼Hash值不同可是校驗是經過的 boolean f = passwordEncoder.matches(password, hashPass); System.out.println(f); } }
二、在AuthorizationServerConfig配置類中配置BCryptPasswordEncoder
//採用bcrypt對密碼進行Hash @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
用戶登陸成功在頁頭顯示當前登陸的用戶名稱。
數據流程以下圖:
一、用戶請求認證服務,登陸成功。
二、用戶登陸成功,認證服務向cookie寫入身份令牌,向redis寫入user_token(身份令牌及受權jwt受權令牌)
三、客戶端攜帶cookie中的身份令牌請求認證服務獲取jwt令牌。
四、客戶端解析jwt令牌,並將解析的用戶信息存儲到sessionStorage中。
jwt令牌中包括了用戶的基本信息,客戶端解析jwt令牌便可獲取用戶信息。
五、客戶端從sessionStorage中讀取用戶信息,並在頁頭顯示。
認證服務對外提供jwt查詢接口,流程以下:
1 、客戶端攜帶cookie中的身份令牌請求認證服務獲取jwt
二、認證服務根據身份令牌從redis中查詢jwt令牌並返回給客戶端。
在認證模塊定義 jwt查詢接口:
@Api(value = "jwt查詢接口",description = "客戶端查詢jwt令牌內容") public interface AuthControllerApi { @ApiOperation("查詢userjwt令牌") public JwtResult userjwt();
在AuthService中定義方法以下:
//從redis查詢令牌 public AuthToken getUserToken(String token){ String userToken = "user_token:"+token; String userTokenString = stringRedisTemplate.opsForValue().get(userToken); if(userToken!=null){ AuthToken authToken = null; try { authToken = JSON.parseObject(userTokenString, AuthToken.class); } catch (Exception e) { LOGGER.error("getUserToken from redis and execute JSON.parseObject error {}",e.getMessage()); e.printStackTrace(); } return authToken; } return null; }
@Override @GetMapping("/userjwt") public JwtResult userjwt() { //獲取cookie中的令牌 String access_token = getTokenFormCookie(); //根據令牌從redis查詢jwt AuthToken authToken = authService.getUserToken(access_token); if(authToken == null){ return new JwtResult(CommonCode.FAIL,null); } return new JwtResult(CommonCode.SUCCESS,authToken.getJwt_token()); } //從cookie中讀取訪問令牌 private String getTokenFormCookie(){ Map<String, String> cookieMap = CookieUtil.readCookie(request, "uid"); String access_token = cookieMap.get("uid"); return access_token; }
使用postman測試
一、請求 /auth/userlogin
觀察cookie是否已存入用戶身份令牌。
二、get請求jwt
操做流程以下:
一、用戶點擊退出,彈出退出確認窗口,點擊肯定
二、退出成功
用戶退出要如下動做:
一、刪除redis中的token
二、刪除cookie中的token
認證服務對外提供退出接口。
@ApiOperation("退出") public ResponseResult logout();
認證服務提供退出接口。
//從redis中刪除令牌 public boolean delToken(String access_token){ String name = "user_token:" + access_token; stringRedisTemplate.delete(name); return true; }
//退出 @Override @PostMapping("/userlogout") public ResponseResult logout() { //取出身份令牌 String uid = getTokenFormCookie(); //刪除redis中token authService.delToken(uid); //清除cookie clearCookie(uid); return new ResponseResult(CommonCode.SUCCESS); } //清除cookie private void clearCookie(String token){ CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, 0, false); }
認證服務默認都要校驗用戶的身份信息,這裏須要將退出url放行。
在WebSecurityConfig類中重寫 configure(WebSecurity web)方法,以下:
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/userlogin","/userlogout"); }
什麼是Zuul?
Spring Cloud Zuul是整合Netflix公司的Zuul開源項目實現的微服務網關,它實現了請求路由、負載均衡、校驗過
慮等 功能。
什麼是網關?
服務網關是在微服務前邊設置一道屏障,請求先到服務網關,網關會對請求進行過慮、校驗、路由等處理。有了服
務網關能夠提升微服務的安全性,網關校驗請求的合法性,請求不合法將被攔截,拒絕訪問。
Zuul與Nginx怎麼配合使用?
Zuul與Nginx在實際項目中須要配合使用,以下圖,Nginx的做用是反向代理、負載均衡,Zuul的做用是保障微服
務的安全訪問,攔截微服務請求,校驗合法性及負載均衡。
Zuul網關具備代理的功能,根據請求的url轉發到微服務,以下圖:
客戶端請求網關/api/learning,經過路由轉發到/learning
客戶端請求網關/api/course,經過路由轉發到/course
在appcation.yml中配置:
zuul: routes: manage‐course: #路由名稱,名稱任意,保持全部路由名稱惟一 path: /course/** serviceId: xc‐service‐manage‐course #指定服務id,從Eureka中找到服務的ip和端口 #url: http://localhost:31200 #也可指定url strip‐prefix: false #true:代理轉發時去掉前綴,false:代理轉發時不去掉前綴 sensitiveHeaders: #默認zuul會屏蔽cookie,cookie不會傳到下游服務,這裏設置爲空則取消默認的黑名 單,若是設置了具體的頭信息則不會傳到下游服務 # ignoredHeaders: Authorization
serviceId:推薦使用serviceId,zuul會從Eureka中找到服務id對應的ip和端口。
strip-prefix: false #true:代理轉發時去掉前綴,false:代理轉發時不去掉前綴,例如,爲true請
求/course/coursebase/get/..,代理轉發到/coursebase/get/,若是爲false則代理轉發到/course/coursebase/get
sensitiveHeaders :敏感頭設置,默認會過慮掉cookie,這裏設置爲空表示不過慮
ignoredHeaders:能夠設置過慮的頭信息,默認爲空表示不過慮任何頭
完整的路由配置
zuul: routes: xc‐service‐learning: #路由名稱,名稱任意,保持全部路由名稱惟一 path: /learning/** serviceId: xc‐service‐learning #指定服務id,從Eureka中找到服務的ip和端口 strip‐prefix: false sensitiveHeaders: manage‐course: path: /course/** serviceId: xc‐service‐manage‐course strip‐prefix: false sensitiveHeaders: manage‐cms: path: /cms/** serviceId: xc‐service‐manage‐cms strip‐prefix: false sensitiveHeaders: manage‐sys: path: /sys/** serviceId: xc‐service‐manage‐cms strip‐prefix: false sensitiveHeaders: service‐ucenter: path: /ucenter/** serviceId: xc‐service‐ucenter sensitiveHeaders: strip‐prefix: false xc‐service‐manage‐order: path: /order/** serviceId: xc‐service‐manage‐order sensitiveHeaders: strip‐prefix: false
Zuul的核心就是過慮器,經過過慮器實現請求過慮,身份校驗等。
自定義過慮器須要繼承 ZuulFilter,ZuulFilter是一個抽象類,須要覆蓋它的四個方法,以下:
一、 shouldFilter:返回一個Boolean值,判斷該過濾器是否須要執行。返回true表示要執行此過慮器,不然不執
行。 二、 run:過濾器的業務邏輯。 三、 filterType:返回字符串表明過濾器的類型,以下 pre:請求在被路由以前
執行 routing:在路由請求時調用 post:在routing和errror過濾器以後調用 error:處理請求時發生錯誤調用
四、 filterOrder:此方法返回整型數值,經過此數值來定義過濾器的執行順序,數字越小優先級越高。
過慮全部請求,判斷頭部信息是否有Authorization,若是沒有則拒絕訪問,不然轉發到微服務。
定義過慮器,使用@Component標識爲bean。
@Component public class LoginFilterTest extends ZuulFilter { private static final Logger LOG = LoggerFactory.getLogger(LoginFilterTest.class); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 2;//int值來定義過濾器的執行順序,數值越小優先級越高 } @Override public boolean shouldFilter() {// 該過濾器須要執行 return true; } @Override public Object run() { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletResponse response = requestContext.getResponse(); HttpServletRequest request = requestContext.getRequest(); //取出頭部信息Authorization String authorization = request.getHeader("Authorization"); if(StringUtils.isEmpty(authorization)){ requestContext.setSendZuulResponse(false);// 拒絕訪問 requestContext.setResponseStatusCode(200);// 設置響應狀態碼 ResponseResult unauthenticated = new ResponseResult(CommonCode.UNAUTHENTICATED); String jsonString = JSON.toJSONString(unauthenticated); requestContext.setResponseBody(jsonString); requestContext.getResponse().setContentType("application/json;charset=UTF‐8"); return null; } return null; } }
測試:
請求:http://localhost:50201/api/course/coursebase/get/4028e581617f945f01617f9dabc40000查詢課程信息