赞
踩
项目pom.xml文件中引入Spring Security和Jwt的依赖坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
整合Redis,将登录用户信息缓存进Redis,整合参考链接部分
0:配置Spring Security
这是Spring Security的参考配置项,主要包括以下内容:URL拦截、匿名用户访问无权限资源处理器(AuthenticationEntryPointHandler)、登出处理器(LogoutSuccessHandler)、登出URL、过滤器(TokenFilter)、UserDetailsService实现类等
import com.liu.gardenia.security.security.filter.TokenFilter; import com.liu.gardenia.security.security.handler.AuthenticationEntryPointHandler; import com.liu.gardenia.security.security.handler.MyLogoutSuccessHandler; import com.liu.gardenia.security.security.service.UserDetailsServiceImpl; import org.springframework.context.annotation.Bean; 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.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * Spring Security配置 * * @author liujiazhong */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { private final AuthenticationEntryPointHandler unauthorizedHandler; private final MyLogoutSuccessHandler logoutSuccessHandler; private final TokenFilter tokenFilter; public SecurityConfig(AuthenticationEntryPointHandler unauthorizedHandler, MyLogoutSuccessHandler logoutSuccessHandler, TokenFilter tokenFilter) { this.unauthorizedHandler = unauthorizedHandler; this.logoutSuccessHandler = logoutSuccessHandler; this.tokenFilter = tokenFilter; } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean @Override public UserDetailsService userDetailsService() { return new UserDetailsServiceImpl(); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * hasRole 如果有参数,参数表示角色,则其角色可以访问 * hasAnyRole 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority 如果有参数,参数表示权限,则其权限可以访问 * hasAnyAuthority 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasIpAddress 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * permitAll 用户可以任意访问 * anonymous 匿名可以访问 * rememberMe 允许通过remember-me登录的用户访问 * denyAll 用户不能访问 * authenticated 用户登录后可访问 * fullyAuthenticated 用户完全认证可以访问(非remember-me下自动登录) * access SpringEl表达式结果为true时可以访问 * anyRequest 匹配所有请求路径 */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers("/api/user/login").anonymous() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/**/*.ico" ).permitAll() .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/api/user/logout").logoutSuccessHandler(logoutSuccessHandler); httpSecurity.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(bCryptPasswordEncoder()); } }
1:实现匿名用户访问无权限资源异常处理器
无权访问时可以根据自己业务需求做相应操作,例如抛出异常、返回提示信息给前端、打印日志等
/**
* @author liujiazhong
*/
@Slf4j
@Component
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = 1L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
log.warn("认证失败,无法访问系统资源:{}", request.getRequestURI());
ServletUtils.renderString(response, "无权访问");
}
}
2:实现登出处理器
实现登出时的相关操作,例如从redis中移除缓存的登陆用户信息、把登出成功的提示信息返回给前端等
/** * @author liujiazhong */ @Slf4j @Component public class MyLogoutSuccessHandler implements LogoutSuccessHandler { private final TokenService tokenService; public MyLogoutSuccessHandler(TokenService tokenService) { this.tokenService = tokenService; } @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { UserInfo userInfo = tokenService.getUserInfo(request); if (Objects.nonNull(userInfo)) { tokenService.removeUserInfo(userInfo.getUuid()); } ServletUtils.renderString(response, "logout success."); } }
3:自定义过滤器进行授权操作
从Redis缓存中取出用户信息,生成Spring Security身份认证令牌放入Security上下文中,这里可以选择不同的授权方式
/** * @author liujiazhong */ @Slf4j @Component public class TokenFilter extends OncePerRequestFilter { private final TokenService tokenService; public TokenFilter(TokenService tokenService) { this.tokenService = tokenService; } @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain) throws IOException, ServletException { log.info("into token filter..."); UserInfo userInfo = tokenService.getUserInfo(request); if (Objects.nonNull(userInfo) && Objects.isNull(SecurityUtils.getAuthentication())) { tokenService.verifyToken(userInfo); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userInfo, null, userInfo.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } }
4:准备登录用户信息实体
这里需要实现Spring Security中的UserDetails接口,重写接口中的一些方法,比如指定用户名和密码等,可以根据业务情况告诉Security当前账户是否过期、是否锁定、是否禁用等信息,还可以直接通过getAuthorities()方法把该账号对应的角色和权限返回给Security,也可以自己手动实现权限验证,我这里选择手动实现
/** * @author liujiazhong */ @Getter @Setter public class UserInfo implements UserDetails { private Long userId; private String username; private String password; private Set<String> permissions; private String uuid; private Long loginTime; private Long expireTime; @JsonIgnore @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { return true; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } }
5:实现UserDetailsService
这里重写接口中的loadUserByUsername()方法,从数据库查询到用户信息和该用户对应的权限列表,返回UserInfo
/** * @author liujiazhong */ @Slf4j @Service public class UserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfo userInfo = userInfo(username); if (Objects.isNull(userInfo)) { throw new RuntimeException("user not found."); } // todo check user: status... return userInfo; } private UserInfo userInfo(String username) { // todo find userInfo from mysql UserInfo userInfo = null; if (Objects.equals("liu", username)) { userInfo = new UserInfo(); userInfo.setUserId(1001L); userInfo.setUsername("liu"); userInfo.setPassword(SecurityUtils.encryptPassword("1111")); userInfo.setPermissions(userPermissionByUserId(userInfo.getUserId())); } return userInfo; } private Set<String> userPermissionByUserId(Long userId) { // todo find permissions from mysql Set<String> permissions = new HashSet<>(1); permissions.add("*:*:*"); return permissions; } }
6:Jwt相关
这里涉及到了Token的生成与解析,用户信息的缓存等操作,RedisCache的实现参考连接部分整合Redis
/** * token验证处理 * * @author liujiazhong */ @Component public class TokenService { protected static final long MILLIS_SECOND = 1000; protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND; private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L; private final RedisCache redisCache; private final TokenConfig tokenConfig; public TokenService(RedisCache redisCache, TokenConfig tokenConfig) { this.redisCache = redisCache; this.tokenConfig = tokenConfig; } public UserInfo getUserInfo(HttpServletRequest request) { String token = getToken(request); if (StringUtils.isEmpty(token)) { return null; } Claims claims = parseToken(token); String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); Object value = redisCache.getCacheObject(userKey); if (Objects.isNull(value)) { return null; } if (value instanceof UserInfo) { return (UserInfo) value; } throw new RuntimeException("UserInfo Cache Type Error."); } public void setUserInfo(UserInfo userInfo) { if (Objects.nonNull(userInfo) && StringUtils.isNotEmpty(userInfo.getUuid())) { refreshToken(userInfo); } } public void removeUserInfo(String uuid) { if (StringUtils.isNotEmpty(uuid)) { String userKey = getTokenKey(uuid); redisCache.deleteObject(userKey); } } public String createToken(UserInfo userInfo) { String uuid = IdUtils.uuid(); userInfo.setUuid(uuid); refreshToken(userInfo); Map<String, Object> claims = new HashMap<>(1); claims.put(Constants.LOGIN_USER_KEY, uuid); return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, tokenConfig.getSecret()).compact(); } public void verifyToken(UserInfo userInfo) { long expireTime = userInfo.getExpireTime(); long currentTime = System.currentTimeMillis(); if (expireTime - currentTime <= MILLIS_MINUTE_TEN) { refreshToken(userInfo); } } public void refreshToken(UserInfo userInfo) { userInfo.setLoginTime(System.currentTimeMillis()); userInfo.setExpireTime(userInfo.getLoginTime() + tokenConfig.getExpireTime() * MILLIS_MINUTE); String userKey = getTokenKey(userInfo.getUuid()); redisCache.setCacheObject(userKey, userInfo, tokenConfig.getExpireTime(), TimeUnit.MINUTES); } public String getUsernameFromToken(String token) { Claims claims = parseToken(token); return claims.getSubject(); } private Claims parseToken(String token) { return Jwts.parser().setSigningKey(tokenConfig.getSecret()).parseClaimsJws(token).getBody(); } private String getToken(HttpServletRequest request) { String token = request.getHeader(tokenConfig.getHeader()); if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) { token = token.replace(Constants.TOKEN_PREFIX, ""); } return token; } private String getTokenKey(String uuid) { return Constants.LOGIN_TOKEN_KEY + uuid; } }
7:鉴权
鉴权操作的实现,从缓存中获取到登录用户信息,判定当前用户拥有的权限中是否包含该资源的权限
/** * @author liujiazhong */ @Slf4j @Service("ps") public class PermissionServiceImpl { private static final String ALL_PERMISSION = "*:*:*"; private final TokenService tokenService; public PermissionServiceImpl(TokenService tokenService) { this.tokenService = tokenService; } public boolean hasPermission(String permission) { if (StringUtils.isBlank(permission)) { return false; } UserInfo info = tokenService.getUserInfo(ServletUtils.getRequest()); if (Objects.isNull(info) || CollectionUtils.isEmpty(info.getPermissions())) { return false; } return check(info.getPermissions(), permission); } private boolean check(Set<String> permissions, String permission) { return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission)); } }
8:给资源加权限
使用@PreAuthorize注解标记资源,“ps”为步骤7中的鉴权实现类,“hasPermission”为鉴权方法,“gardenia:demo:info”为自定义的资源权限
/** * @author liujiazhong */ @RestController @RequestMapping("api/demo") public class DemoController { @GetMapping("hello") public String hello() { return "hello"; } @PreAuthorize("@ps.hasPermission('gardenia:demo:info')") @GetMapping("info") public String info() { return "liu"; } }
9:登录
认证通过后直接返回Token
/** * @author liujiazhong */ @Slf4j @Service public class UserLoginServiceImpl implements UserLoginService { private final TokenService tokenService; private final AuthenticationManager authenticationManager; public UserLoginServiceImpl(TokenService tokenService, AuthenticationManager authenticationManager) { this.tokenService = tokenService; this.authenticationManager = authenticationManager; } @Override public String login(String username, String password) { Authentication authentication; try { authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); } catch (Exception e) { if (e instanceof BadCredentialsException) { throw new PasswordException(); } else { throw new RuntimeException(e.getMessage()); } } UserInfo userInfo = (UserInfo) authentication.getPrincipal(); return tokenService.createToken(userInfo); } }
补充上文中使用到的几个自定义工具类
IdUtils
public class IdUtils {
public static String uuid() {
return UUID.randomUUID().toString();
}
}
SecurityUtils
public class SecurityUtils { public static Authentication getAuthentication() { return SecurityContextHolder.getContext().getAuthentication(); } public static String encryptPassword(String password) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder.encode(password); } public static boolean validPassword(String password, String encodedPassword) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder.matches(password, encodedPassword); } }
ServletUtils
@Slf4j public class ServletUtils { public static HttpServletRequest getRequest() { return getRequestAttributes().getRequest(); } public static ServletRequestAttributes getRequestAttributes() { RequestAttributes attributes = RequestContextHolder.getRequestAttributes(); return (ServletRequestAttributes) attributes; } public static void renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (Exception e) { log.error("ServletUtils.renderString exception...", e); } } }
TokenConfig
/**
* @author liujiazhong
*/
@Data
@Component
@ConfigurationProperties("gardenia.security.token")
public class TokenConfig {
private String header;
private String secret;
private Integer expireTime;
}
gardenia:
security:
token:
header: Authorization
secret: secret
expire-time: 30
SpringBoot整合Spring Data Redis:https://blog.csdn.net/momo57l/article/details/105427898
Spring Security:https://spring.io/projects/spring-security#overview
CSRF:https://docs.spring.io/spring-security/site/docs/5.3.2.BUILD-SNAPSHOT/reference/html5/#csrf
JWT:https://jwt.io/
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。