四、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; } }
-