四、SpringSecurity前後端分離下的方案
1、基本原理:
- 登入過程是SpringSecurity原理,然後驗證成功後利用Jwt生産使用者Token,用Key為Token,Value為使用者資訊存入Redis中完成首次登入。
- 之後的請求中,過濾器去判斷請求中是否攜帶了Token,如果有就直接放行繼續接下來的操作,否則無權通路需要登入。
- 思路流程圖:
2、詳細代碼流程
- 編寫核心配置了
-
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private UserDetailsService userDetailsService; @Autowired private TokenFilter tokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { //關閉CSRF防護 http.csrf().disable(); //基于Token,不需要Session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //對于路徑請求 http.authorizeRequests() .antMatchers("/user/all","/user/login").permitAll()//這些路徑放行 .anyRequest()//其他任何請求 .authenticated();//都需要驗證 http.formLogin() //登入的路徑請求,usernameParameter設定了表單中username的參數名,passwordParameter設定了表單中password的參數名 .loginProcessingUrl("/user/login").usernameParameter("username").passwordParameter("password") //成功會進入這個處理器 .successHandler(authenticationSuccessHandler) //失敗會進入這個處理器 .failureHandler(authenticationFailureHandler) //沒有權限會進入這個處理器 .and().exceptionHandling().authenticationEntryPoint(authenticationEntryPoint); //退出的路徑請求,logoutSuccessHandler表示退出成功後進入這個處理器 http.logout().logoutUrl("user/logout").logoutSuccessHandler(logoutSuccessHandler); //表示在UsernamePasswordAuthenticationFilter這個過濾器執行前,先執行tokenFilter過濾器 http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return MD5.encrypt(charSequence.toString()); } @Override public boolean matches(CharSequence charSequence, String s) { String encode = MD5.encrypt(charSequence.toString()); return s.equals(encode); } }); } }
-
- 編寫TokenFilter過濾器
- 主要功能:驗證前去過濾請求中是否包含了token,如果包含了就從Redis中擷取使用者資訊,否則繼續驗證
-
@Component @Slf4j @Order(value = Integer.MAX_VALUE - 2) public class TokenFilter extends OncePerRequestFilter { public static final String TOKEN_KEY = "Authorization"; @Autowired private TokenService tokenService; @Autowired private UserDetailsService userDetailsService; private static final Long MINUTES_10 = 10 * 60 * 1000L; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //從請求中擷取token String token = getToken(request); //如果token不為空 if(!StringUtils.isEmpty(token)){ //就從redis中擷取使用者資訊 LoginUser loginUser = tokenService.getLoginUser(token); if(loginUser != null ){ //如果不為空,就檢查緩存中的時間是否小于10分鐘,如果小于就更新緩存 loginUser =checkLoginTime(loginUser); //把使用者對象封裝成UsernamePasswordAuthenticationToken對象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities()); //把UsernamePasswordAuthenticationToken對象放入Security上下文中 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } filterChain.doFilter(request,response); } public static String getToken(HttpServletRequest request) { String token = request.getParameter(TOKEN_KEY); if (StringUtils.isEmpty(token)) { token = request.getHeader(TOKEN_KEY); } if (StringUtils.isEmpty(token)) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (TOKEN_KEY.equals(cookie.getName())) { token = cookie.getValue(); break; } } } } return token; } /** * 校驗時間<br> * 過期時間與目前時間對比,臨近過期10分鐘内的話,自動重新整理緩存 * * @param loginUser * @return */ public LoginUser checkLoginTime(LoginUser loginUser) { long expireTime = loginUser.getExpireTime(); long currentTime = System.currentTimeMillis(); if (expireTime - currentTime <= MINUTES_10) { String token = loginUser.getToken(); loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername()); loginUser.setToken(token); tokenService.refresh(loginUser); } return loginUser; } }
- 編寫Handler配置類
-
@Configuration public class SecurityHandlerConfig { @Autowired private TokenService tokenService; //登入成功後的處理器 @Bean public AuthenticationSuccessHandler loginSuccsessHandler(){ return (request, response, authentication) -> { //從SpringSecurity上下文中擷取已經通過認證的使用者對象 LoginUser loginUser = (LoginUser) authentication.getPrincipal(); //登入成功的相應邏輯操作 loginSuccessReturn(request,response,loginUser); }; } public void loginSuccessReturn(HttpServletRequest request, HttpServletResponse response, LoginUser loginUser) { //響應容器 Map map = new HashMap(); //根據使用者生産一個Token,并存入redis Token token = tokenService.saveToken(loginUser); //放入加密token map.put("id", loginUser.getId()); map.put("token", token.getToken()); Cookie cookie = new Cookie("token", map.get("token").toString()); cookie.setPath("/"); response.addCookie(cookie); //封裝傳回 responseJson(response, HttpStatus.OK.value(), map); } //登入失敗的處理器 @Bean public AuthenticationFailureHandler loginFailureHandler(){ return (request, response, exception) -> { String msg; if(exception instanceof BadCredentialsException){ msg = "密碼錯誤!"; } else { msg = exception.getMessage(); } Map<String, Object> data = new HashMap<>(); data.put("loginType", 5); data.put("loginMsg", msg); responseJson(response, HttpStatus.OK.value(), data); }; } //無權限處理器 @Bean public AuthenticationEntryPoint authenticationEntryPoint(){ return (request, response, exception) -> { String url = request.getRequestURI(); if(url.endsWith(".html")) { response.sendRedirect("/"); } else { responseJson(response, HttpStatus.UNAUTHORIZED.value(), "請先登入"); } }; } //登出處理器 @Bean public LogoutSuccessHandler logoutSuccessHandler(){ return (request, response, authentication) -> { String token = TokenFilter.getToken(request); tokenService.deleteToken(token); responseJson(response, HttpStatus.OK.value(), "退出成功"); }; } //封裝傳回 public void responseJson(HttpServletResponse response, int status, Object data) { try { response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "*"); response.setContentType("application/json;charset=UTF-8"); response.setStatus(status); response.getWriter().write(JSONObject.toJSONString(data)); } catch (IOException e) { e.printStackTrace(); } } }
-
- 編寫UserDetailsService接口實作類
-
@Service public class MyUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = null; if (StringUtils.isEmpty(username) || (user = getByUserName(username)) == null) { // 傳回使用者名不存在 // 抛出異常後 架構會去調用 loginFailureHandler() throw new RuntimeException("使用者名不存在"); } LoginUser loginUser = new LoginUser(); if(user != null ){ BeanUtils.copyProperties(user,loginUser); //設定使用者權限,可自行修改從資料庫擷取 if(loginUser.getUsername().equals("admin")){ List<String> authorities = new ArrayList<String>(); authorities.add("ROLE_admin"); loginUser.setPermissionValueList(authorities); } else { List<String> authorities = new ArrayList<String>(); authorities.add("ROLE_admin1"); loginUser.setPermissionValueList(authorities); } } return loginUser; } private User getByUserName(String username) { QueryWrapper<User> query = new QueryWrapper<>(); query.lambda().eq(User::getUsername,username); User user = userMapper.selectOne(query); return user; }
-
- 編寫UserDetails接口實體類
-
@Data @AllArgsConstructor @NoArgsConstructor public class LoginUser extends User implements UserDetails { private String token; private Long loginTime; private Long expireTime; private List<String> permissionValueList; private List<GrantedAuthority> authorities; //獲得使用者權限 @Override public Collection<? extends GrantedAuthority> getAuthorities() { authorities = new ArrayList<>(); for (String permissionValue : permissionValueList) { if(StringUtils.isEmpty(permissionValue))continue; SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue); authorities.add(authority); } return authorities; // return null; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
-
//User實體類對應資料庫表的實體類 @Data public class User { private Integer id; private String username; private String password; }
-
- 提供個人寫的TokenService供大家參考,也可自行編寫(主要邏輯是生産Token,存入Redis)
-
@Service @Slf4j public class TokenService { /** * token過期秒數 */ @Value("${token.expire.seconds}") private Integer expireSeconds; /** * 私鑰 */ @Value("${token.jwtSecret}") private String jwtSecret; @Autowired private JedisClient jedisClient; private static Key KEY = null; private static final String LOGIN_USER_KEY = "LOGIN_USER_KEY"; /** * 儲存使用者資訊至緩存,key為uuid,傳回生成token * @param loginUser * @return */ public Token saveToken(LoginUser loginUser) { loginUser.setToken(UUID.randomUUID().toString()); loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000); jedisClient.setnx(loginUser.getToken(), JSONObject.toJSONString(loginUser), Long.valueOf(expireSeconds * 1000)); String jwtToken = createJWTToken(loginUser); return new Token(jwtToken, loginUser.getLoginTime()); } /** * 生成jwt * * @param loginUser * @return */ private String createJWTToken(LoginUser loginUser) { Map<String, Object> claims = new HashMap<>(); // 放入一個随機字元串,通過該串可找到登陸使用者 claims.put(LOGIN_USER_KEY, loginUser.getToken()); String jwtToken = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, getKeyInstance()) .compact(); return jwtToken; } /** * 重新整理緩存 * @param loginUser */ public void refresh(LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000); jedisClient.setnx(loginUser.getToken(), JSONObject.toJSONString(loginUser), Long.valueOf(expireSeconds * 1000)); } /** * 根據jwt擷取登入使用者資訊 * @param jwtToken * @return */ public LoginUser getLoginUser(String jwtToken) { String uuid = getUUIDFromJWT(jwtToken); if (uuid != null) { return toLoginUser(uuid); } return null; } /** * 删除緩存中的使用者資訊 * @param jwtToken * @return */ public boolean deleteToken(String jwtToken) { String uuid = getUUIDFromJWT(jwtToken); if (uuid != null) { LoginUser loginUser = toLoginUser(uuid); if (loginUser != null) { jedisClient.del(uuid); return true; } } return false; } /** * 加鎖擷取加密key * @return */ private Key getKeyInstance() { if (KEY == null) { synchronized (TokenService.class) { if (KEY == null) { // 雙重鎖 byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtSecret); KEY = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName()); } } } return KEY; } /** * 解析jwt擷取uuid * @param jwt * @return */ private String getUUIDFromJWT(String jwt) { if ("null".equals(jwt) || StringUtils.isEmpty(jwt)) { return null; } Map<String, Object> jwtClaims = null; try { jwtClaims = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwt).getBody(); if (jwtClaims.containsKey(LOGIN_USER_KEY)) { return (String) jwtClaims.get(LOGIN_USER_KEY); } return null; } catch (ExpiredJwtException e) { log.error("token:{}已過期", jwt); } catch (Exception e) { log.error("解析token異常,token:{}", e); } return null; } /** * 根據key擷取緩存中的使用者資訊 * @param key 緩存key * @return */ private LoginUser toLoginUser(String key) { if (key == null) { return null; } String value = jedisClient.get(key); // 校驗是否已過期,已過期value為null if (StringUtils.isNotEmpty(value)) { LoginUser loginUser = JSONObject.parseObject(value, LoginUser.class); return loginUser; } return null; } }
-