目录
前言
Spring Security是一个基于Spring框架的安全框架,它提供了一套完整的认证和授权机制,可以方便地实现登录功能
核心概念
1.认证(Authentication):验证用户的身份是否有效。
2.授权(Authorization):授予用户访问资源的权限。
3.安全上下文(Security Context):在应用程序执行期间,Spring Security将当前
用户的安全信息存储在安全上下文中。
4.过滤器链(Filter Chain):Spring Security通过一组过滤器来拦截HTTP请求,并
对请求进行认证和授权处理。
5.访问控制(Access Control):Spring Security可以根据配置的访问规则来限制用
户对资源的访问。
6.Remember Me:记住用户的身份信息,使得用户在下次访问应用程序时不需要再次输入
用户名和密码。
7.Session管理:Spring Security提供了对用户Session的管理机制,可以控制用户的
登录和退出操作,并提供了一些额外的安全功能。
环境配置
在开始Spring Security之前,需要配置好以下环境:
1.JDK 1.8或以上
2.Maven 3.0或以上
3.Spring Boot 2.1.4或以上
4.IDE(推荐使用IntelliJ IDEA)
一、导入依赖
首先需要在Spring Boot项目中添加Spring Security的依赖。在pom.xml文件中添加以下依赖:
<!-- spring security 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
二、工具类
2.1、Reids工具类
看我另一篇文章
2.2、token工具类
把处理token的一些操作封装起来了,直接拿去用就行
@Component
public class TokenService {
/** 令牌秘钥 */
@Value("${token.secretKey}")
private String secretKey;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
@Resource
private RedisCache redisCache;
/**
* 根据token获取登录用户
* @param token
*/
public Object getLoginUserByToken(String token) {
if (!org.springframework.util.StringUtils.isEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
String tokenUuidKey = ConstantsCache.CACHE_KEY_TOKEN_LOGIN + SecurityUtils.decryptSecret(token, secretKey);
Object loginUser = redisCache.getCacheObject(tokenUuidKey);
// 获取到登录用户,说明用户有效,则redis续签
if (loginUser != null) {
GlobalConfig globalConfig = SpringUtil.getBean("globalConfig");
long expireTime = Long.valueOf(globalConfig.getGlobalConfigMap().get("CORE_SYS_CONFIG:core.token.expireTime").toString());
redisCache.expire(tokenUuidKey, expireTime, TimeUnit.MINUTES);
}
return loginUser;
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtil.isNotEmpty(token)) {
String tokenUuid = SecurityUtils.decryptSecret(token, secretKey);
String userJson = redisCache.getCacheObject(getTokenUuidKey(tokenUuid));
if (StringUtil.isNotBlank(userJson)) {
return JSONObject.parseObject(userJson).toJavaObject(LoginUser.class);
}
}
return null;
}
/**
* 创建令牌
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser) {
String tokenUuid = SeqUtil.fastUUID();
String token = SecurityUtils.encryptSecret(tokenUuid, secretKey);
loginUser.setTokenUuid(tokenUuid);
loginUser.setToken(token);
// 记录登录信息
setUserBrowser(loginUser);
// 登录用户存储到redis
setRedisToken(loginUser);
// 根据UUID生成Token
return token;
}
/**
* 写入令牌到redis
* @param loginUser 登录信息
*/
public void setRedisToken(LoginUser loginUser) {
GlobalConfig globalConfig = SpringUtil.getBean("globalConfig");
int expireTime = Integer.valueOf(globalConfig.getGlobalConfigMap().get("CORE_SYS_CONFIG:core.token.expireTime").toString());
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String tokenUuidKey = getTokenUuidKey(loginUser.getTokenUuid());
redisCache.setCacheObject(tokenUuidKey, JSONObject.toJSONString(loginUser), expireTime, TimeUnit.MINUTES);
}
/**
* 刷新令牌
* @param loginUser
*/
public void refreshRedisToken(LoginUser loginUser) {
GlobalConfig globalConfig = SpringUtil.getBean("globalConfig");
long expireTime = Long.valueOf(globalConfig.getGlobalConfigMap().get("CORE_SYS_CONFIG:core.token.expireTime").toString());
// 根据uuid将loginUser缓存
String tokenUuidKey = getTokenUuidKey(loginUser.getTokenUuid());
redisCache.expire(tokenUuidKey, expireTime, TimeUnit.MINUTES);
}
/**
* 删除令牌
*/
public void delRedisToken(String tokenUuid) {
if (StringUtil.isNotEmpty(tokenUuid)) {
redisCache.deleteObject(getTokenUuidKey(tokenUuid));
}
}
/**
* 获取请求token
* @param request
* @return token
*/
public String getToken(HttpServletRequest request) {
String token = request.getHeader(Constants.TOKEN_HEADER);
if (StringUtil.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
/**
* 生成登录用户缓存key
* @param tokenUuid
* @return
*/
private String getTokenUuidKey(String tokenUuid) {
return ConstantsCache.CACHE_KEY_TOKEN_LOGIN + tokenUuid;
}
/**
* 用户登录信息
*/
private void setUserBrowser(LoginUser loginUser) {
try {
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtil.getRequest().getHeader("User-Agent"));
String ip = IpUtil.getIpAddr(ServletUtil.getRequest());
loginUser.setIpaddr(ip);
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
}
二、自定义UserDetails
需要实现UserDetails接口,并重写其中方法和定义一下自己需要用到的属性。
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/** 用户唯一标识 */
private String tokenUuid;
private String token;
/** 登陆时间 */
private Long loginTime;
/** 过期时间 */
private Long expireTime;
/** 登录IP地址 */
private String ipaddr;
/** 浏览器类型 */
private String browser;
/** 操作系统 */
private String os;
/** 用户信息 */
private SysUser user;
public LoginUser() {
}
public LoginUser(SysUser user) {
this.user = user;
}
@JsonIgnore
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
三、相关配置类
2.1 、用户认证类
需要实现UserDetailsService接口,并重写其中的loadUserByUsername方法。在这个方法中,你可以从数据库或其他数据源中获取用户信息,并返回一个实现了UserDetails接口的对象。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询数据库需要更加实际情况修改
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), Collections.emptyList());
}
}
2.2、退出处理类
需要实现LogoutSuccessHandler接口,并重写其中的onLogoutSuccess方法。在这个方法中,你可以对退出登录进行处理。
/**
* 自定义退出处理类 返回成功
*
*/
@Configuration
public class AuthTokenLogout implements LogoutSuccessHandler {
@Resource
private TokenService tokenService;
/**
* 退出处理
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtil.isNotNull(loginUser)) {
// 删除用户缓存记录
tokenService.delRedisToken(loginUser.getTokenUuid());
}
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSON.toJSONString(AjaxResult.error(ContantsHttp.SUCCESS, "退出成功")));
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.3、认证失败处理类
需要实现AuthenticationEntryPoint, Serializable接口,并重写其中的commence方法。在这个方法中,对认证失败的请求进行处理。
/**
* 认证失败处理类 返回未授权
*/
@Component
public class AuthTokenEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = -8970718410437077606L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSON.toJSONString(AjaxResult.error(ContantsHttp.UNAUTHORIZED, "请求访问:" + request.getRequestURI() + ",认证失败,无法访问系统资源")));
}
}
2.4、token认证过滤器
需要实现OncePerRequestFilter,并重写其中的doFilterInternal方法。在这个方法中,会拦截所有请求,在这里对请求是否携带token和是否在白名单做判断。
/**
* token过滤器 验证token有效性
*/
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Autowired
SessionRegistry sessionRegistry;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 白名单处理
List<String> whiteList = new ArrayList<>();
if (!CollectionUtils.isEmpty(whiteList) && isSkipUrl(request.getRequestURI(), whiteList)) {
chain.doFilter(request, response);
} else {
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser == null) {
setResponse(response, 401, "401 未登录或登录已过期");
return;
}
if (StringUtil.isNull(SecurityUtils.getAuthentication())) {
tokenService.refreshRedisToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
/**
* 判断当前访问的url是否开头URI是在配置的忽略url列表中
*/
private boolean isSkipUrl(String url, List<String> whiteList) {
for (String skipAuthUrl : whiteList) {
if (url.startsWith(skipAuthUrl)) {
return true;
}
}
return false;
}
/**
* 设置响应信息
*/
private void setResponse(HttpServletResponse response, int code, String msg) {
try {
byte[] responseByte = msg.getBytes(StandardCharsets.UTF_8);
response.setStatus(code);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
writer.write(new String(responseByte));
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.5、Spring Security配置类
Spring Security和核心配置类,需要继承WebSecurityConfigurerAdapter类,在这里开启Spring Security并且做一些相关配置。
/**
* spring security配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AuthTokenSecurity extends WebSecurityConfigurerAdapter {
/** 自定义用户认证逻辑 */
@Resource
private UserDetailsService userDetailsService;
/** 退出处理类 */
@Resource
private AuthTokenLogout logoutSuccessHandler;
/** token认证过滤器 */
@Resource
private AuthTokenFilter authTokenFilter;
/** 认证失败处理类 */
@Resource
private AuthTokenEntryPoint authTokenEntryPoint;
/** 跨域过滤器 */
@Resource
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 AuthenticationManager
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity .csrf().disable() .headers().frameOptions().disable();
// 退出处理类
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 认证失败处理类
httpSecurity.exceptionHandling().authenticationEntryPoint(authTokenEntryPoint);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, AuthTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
httpSecurity.sessionManagement().maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry());
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
2.5.1、JWT过滤器
JWT是Json Web Token,也就是通过JSON的形式作为web应用中的令牌,用于在各方之间安全的将信息作为JSON对象传输,在传输过程中还可以完成数据加密、签名等相关处理。
在Spring Security配置类的configure方法添加
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity .csrf().disable() .headers().frameOptions().disable();
// 退出处理类
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 认证失败处理类
httpSecurity.exceptionHandling().authenticationEntryPoint(authTokenEntryPoint);
// 添加JWT filter
httpSecurity.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, AuthTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
httpSecurity.sessionManagement().maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry());
}
导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
2.5.2、 跨域处理
在Spring Security配置类中添加
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 对接口配置跨域设置
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
总结
本文主要讲解如何通过Spring Security实现认证、过滤器、安全上下文,实现了基本的登录功能,希望以上内容对你有帮助!