1. spring security的介绍
spring security是一个安全管理框架,源自 Spring 家族,可以和 Spring 框架无缝整合。其主要功能有:
- 认证
也就是你进行访问一些网站的时候需要进行登陆之后才能够访问,不登陆的话是无法访问的 - 授权
也就是当前登陆的角色具有访问哪些功能的权限,只有你有相关的权限才能够进行某些操作,不具有这些权限则禁止操作 - 攻击防护
也就是其能够防止csrf、cors、xss的攻击
其中spring security的实现原理主要就是通过一个个拦截器组成的拦截器链来进行实现的。其中用户在访问网站的时候进入一个个拦截器中,在拦截器中进行相关的操作。
2. spring security主要拦截器链的介绍
在引入spring security之后,通过debug我们可以看到其默认的过滤器链是由下面的十六个过滤器组成的:
这里主要学的是如何使用spring security及逆行认证和授权,所以其最主要的是过滤器链中的UsernamePasswordAuthenticationFilter,ExceptionTranslationFilter,FilterSecurityInterceptor这三个过滤器.
UsernamePasswordAuthenticationFilter:这个过滤器从名字就可以看出来其主要是处理用户名和密码的过滤器,就是用户输入用户名和密码在这个过滤器中进行逻辑比对。默认是通过获取本地的用户名和密码。其与spring Security的认证功能有关。
ExceptionTranslationFilter:这个过滤器主要是处理异常过滤器,就是在运行过程中有什么异常都会到这个过滤器中,由这个过滤器进行处理
FilterSecurityInterceptor:这个过滤器主要是一些权限认证的过滤器,与spring security的授权功能有关。
- 认证的主要流程
通过看UsernamePasswordAuthenticationFilter的源码可以得到用户认证过程中的流程图如下所示:
1.我们可以看到UsernamePasswordAuthenticationFilter的主要执行方法如下:
可以从上面看出其将用户名和密码封装成了UsernamePasswordAuthenticationToken,其父类的父类是Authentication对象,然后调用了AuthenticationManager的authenticate()方法。@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username.trim() : ""; String password = obtainPassword(request); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
2.接着往下面看可以看到在AuthenticationManager的authenticate()方法中接着调用了一个AuthenticationProvider的authenticate()的方法:
3.在下面就到AbstractUserDetailsAuthenticationProvider中的authenticate方法了,在这个方法中主要是调用了UserDetailService的loadUserByUserName方法:@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size(); for (AuthenticationProvider provider : getProviders()) { // 此处省略了一些逻辑 ... try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { prepareException(ex, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw ex; } catch (AuthenticationException ex) { lastException = ex; } } // 此处也省略了一些逻辑 ... }
可以从上面的retrieveUser方法中看到其主要是通过调用UserDetailsService的loadUserByUsername方法来获得用户信息@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); String username = determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } // 此处省略了一些代码 ... } retrieveUser方法: @Override protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
4.而UserDetailsService的loadUserByUsername方法默认是从本地获取到用户的一些信息,然后封装成UserDetails对象进行返回,这里可以在配置文件中配置你自己的用户名或密码
5.获取到用户的信息之后再重新到第三步的AbstractUserDetailsAuthenticationProvider中执行后面的操作,进行一些密码的比较等操作。@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetails user = this.users.get(username.toLowerCase()); if (user == null) { throw new UsernameNotFoundException(username); } return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities()); }
6.如果比较通过之后则会返回一个Authentication对象,其实也就是Authentication对象的子类UsernamePasswordAuthenticationToken对象@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 此处省略了一些代码 ... try { this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { throw ex; } // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } // 此处省略了一些代码 ... } additionalAuthenticationChecks方法: @Override @SuppressWarnings("deprecation") protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { this.logger.debug("Failed to authenticate since no credentials provided"); throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { this.logger.debug("Failed to authenticate since password does not match stored value"); throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 此处省略了代码 ... return createSuccessAuthentication(principalToReturn, authentication, user); } createSuccessAuthentication方法: protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // Ensure we return the original credentials the user supplied, // so subsequent attempts are successful even with encoded passwords. // Also ensure we return the original getDetails(), so that future // authentication events after cache expiry contain the details UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); this.logger.debug("Authenticated user"); return result; }
通过上面的分析可以看出来,spring security默认的认证是通过读取本地的用户名和密码来进行登陆的,与我们想要实现的功能不符,我们需要的是通过读取数据库中的用户名和密码来进行登陆。所以这边需要我们自己来实现上面流程中的获取用户信息服务的UserDetailService接口从其中获取用户的信息。另外我们要实现的是前后端分离的需求,所以我们这边要通过设置一个登陆接口让前端请求这个接口,然后我们在这个接口中来调用ProviderManager,而不是在通过UsernamePasswordAuthenticationFilter过滤器来调用。因此认证的思路可以如下图所示:
因此从上面的图中可以看出我们主要做的就是实现一个登录接口,在登陆接口的实现层进行保存用户相关信息,然后自定义UserDetailService的实现类,实现从数据库中查询用户信息与权限的功能就行了。
- 授权的主要流程
根据上面的过滤器链可以知道spring security会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息,查看当前用户是否有访问当前资源所需要的权限。因此我们也要把权限信息存入Authentication中,然后设置访问接口需要的权限即可。
具体信息可以在FilterSecurityInterceptor$authenticateIfRequired方法中看到。
3. 整合spring security的过程
- 1.引入依赖
使用spring boot整合spring security的过程第一步也是需要引入spring security的相关依赖,
这里只是个spring security相关的依赖,由于项目中还用到了redis,mybatis,jwt等相关的功能,所以还需要引入这些有关的依赖<!--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> <!--fastjson依赖--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.71</version> </dependency> <!--jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
- 2.准备一些工具类
在项目中用到了redis,jwt等一些功能,所以这边先提前准备好这些功能的一些工具类和配置等信息1.redis配置类 package com.mcj.music.config; import com.mcj.music.utils.FastJsonRedisSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author mcj * @date 2022/11/19 17:26 * @description redis配置类 */ @Configuration public class RedisConfig { @Bean @SuppressWarnings(value = {"unchecked", "rawtypes"}) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } } 2.使用fastjson序列化工具类 package com.mcj.music.utils; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; import java.nio.charset.Charset; /** * @author mcj * @date 2022/11/19 16:58 * @description redis使用fastJson序列化 */ public class FastJsonRedisSerializer<T> implements RedisSerializer<T> { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); private Class<T> clazz; static { ParserConfig.getGlobalInstance().setAutoTypeSupport(true); } public FastJsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) throws SerializationException { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET); } @Override public T deserialize(byte[] bytes) throws SerializationException { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, DEFAULT_CHARSET); return JSON.parseObject(str, clazz); } protected JavaType getJavaType(Class<?> clazz) { return TypeFactory.defaultInstance().constructType(clazz); } } 3.redis缓存工具类 package com.mcj.music.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.util.*; import java.util.concurrent.TimeUnit; /** * @author mcj * @date 2022/11/19 19:49 * @description redis缓存工具类 */ @Component @SuppressWarnings(value = {"unchecked", "rawtypes"}) public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 缓存基本的对象,Integer、String、实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param <T> */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 缓存基本的对象,Integer, String, 实体类等 * * @param key 缓存的键值 * @param value 缓存的值 * @param timeout 时间 * @param timeUnit 时间颗粒度 * @param <T> */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 设置有效时间 * * @param key redis键 * @param timeout 超时时间 * @return true:设置成功,false:设置失败 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 设置有效时间 * * @param key redis键 * @param timeout 超时时间 * @param unit 时间单位 * @return true:成功,false:失败 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 获取缓存的基本对象 * * @param key 缓存的键值 * @param <T> * @return 缓存键值对应的数据 */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operations = redisTemplate.opsForValue(); return operations.get(key); } /** * 删除单个对象 * * @param key * @return */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 删除集合对象 * * @param collection 多个对象 * @return */ public long deleteObject(final Collection collection) { return redisTemplate.delete(collection); } /** * 缓存List数据 * * @param key 缓存的键值 * @param dataList 待缓存的list数据 * @param <T> * @return 缓存的数量 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 获取缓存的list 对象 * * @param key 缓存的键值 * @param <T> * @return 缓存键值对应的数据 */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 缓存set * * @param key 缓存键值 * @param dataSet 缓存的数据 * @param <T> * @return 缓存数据的对象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperations = redisTemplate.boundSetOps(key); Iterator<T> iterator = dataSet.iterator(); while (iterator.hasNext()) { setOperations.add(iterator.next()); } return setOperations; } /** * 缓存map对象 * * @param key 缓存键值 * @param dataMap 缓存的对象 * @param <T> */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 获得缓存的map * * @param key 键值 * @param <T> * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入数据 * * @param key redis键 * @param hKey hash 键 * @param value 值 * @param <T> */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 获取Hash中的数据 * * @param key redis键 * @param hKey hash键 * @param <T> * @return hash中的对象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> hashOperations = redisTemplate.opsForHash(); return hashOperations.get(key, hKey); } /** * 删除hash中的数据 * * @param key * @param hKey */ public void delCacheMapValue(final String key, final String hKey) { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.delete(key, hKey); } /** * 获取多个Hash中的数据 * * @param key redis键 * @param hKeys hash键集合 * @param <T> * @return */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 获得缓存的基本对象列表 * * @param pattern 字符串前缀 * @return 对象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } } 4.jwt工具类 package com.mcj.music.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Date; import java.util.UUID; /** * @author mcj * @date 2022/11/19 19:28 * @description JWT工具类 */ public class JwtUtil { //有效期 public static final Long JWT_TTL= 60 * 60 * 1000L; // 配置密钥明文 public static final String JWT_KEY = "mcj"; public static String getUUID() { String token = UUID.randomUUID().toString().replaceAll("-", ""); return token; } /** * 生成jwt * @param subject token中要存放的数据(json格式) * @return */ public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID()); // 设置过期时间 return builder.compact(); } /** * 生成jwt * @param subject token中要存的数据(json格式) * @param ttlMillis token超时事件 * @return */ public static String createJWT(String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID()); // 设置过期时间 return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if (ttlMillis == null) { ttlMillis = JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) // 唯一的id .setSubject(subject) // 主题,可以是json数据 .setIssuer("mcj") // 签发者 .setIssuedAt(now) // 签发时间 .signWith(signatureAlgorithm, secretKey) // 使用HS256对称加密算法签名,第二个参数为密钥 .setExpiration(expDate); // 过期时间 } /** * 创建token * @param id * @param subject * @param ttlMillis * @return */ public static String createJWT(String id, String subject, Long ttlMillis) { JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id); // 设置过期时间 return builder.compact(); } /** * 生成加密后的密钥 secretKey * @return */ public static SecretKey generalKey() { byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); } /** * 解析 * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(jwt) .getBody(); } } 5.web的工具类 package com.mcj.music.utils; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author mcj * @date 2022/11/19 20:20 * @description */ public class WebUtils { /** * 将字符串渲染到客户端 * * @param response 渲染对象 * @param string 带渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().println(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
- 3.自定义UserDetailService的实现类
由于我们的需求是从数据库中进行查询登陆用户的信息与用户拥有的权限,而这些操作又都是在UserDetailService的实现类中进行的,所以这边我们应该重新实现UserDetailService方法,在其中写这些逻辑
其中adminMapper与menuMapper则是用来从数据库中查询当前登陆用户信息与权限的dao层,这边就是数据库基本的select语句了,这里就不贴代码了。package com.mcj.music.service.impl; import com.mcj.music.dao.AdminMapper; import com.mcj.music.dao.MenuMapper; import com.mcj.music.domain.Admin; import com.mcj.music.domain.LoginUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; import java.util.Objects; /** * @author mcj * @date 2022/11/20 9:51 * @description 登陆查询用户信息实现类 */ @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private AdminMapper adminMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 Admin admin = adminMapper.queryUserByUsername(username); // 如果没有查询到用户抛异常 if (Objects.isNull(admin)) { throw new RuntimeException("用户名或密码错误"); } // 查询对应的权限信息 List<String> list = menuMapper.selectPermsByUserId(admin.getId()); // 把数据封装成UserDetails return new LoginUser(admin, list); } }
- 4.实现UserDetails接口
由于UserDetailService返回给调用它的上一级都是UserDetails数据类型的,所以这边在自定义UserDetailService的实现类中也会返回UserDetails数据类型,由于默认的UserDetails数据类型不满足我们的需求,所以这边需要进行实现一下:package com.mcj.music.domain; import com.alibaba.fastjson.annotation.JSONField; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.text.SimpleDateFormat; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** * @author mcj * @date 2022/11/20 9:59 * @description 用登陆相关的实体 */ @Data @NoArgsConstructor public class LoginUser implements UserDetails { private Admin admin; /** * 存储权限信息 */ private List<String> permissions; /** * 存储spring Security需要的权限列表,不需要其序列化 */ @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; public LoginUser (Admin admin, List<String> permissions) { this.admin = admin; this.permissions = permissions; } /** * 权限列表 * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null) { return authorities; } // 把permissions中String类型的权限信息封装为SimpleGrantedAuthority authorities = permissions.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return authorities; } /** * 密码 * @return */ @Override public String getPassword() { return admin.getPassword(); } /** * 用户名 * @return */ @Override public String getUsername() { return admin.getName(); } /** * 判断账号是否过期 * @return */ @Override public boolean isAccountNonExpired() { return true; } /** * 账号是否没有锁定 * @return */ @Override public boolean isAccountNonLocked() { return true; } /** * 是否没有超时 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 是否可用 * @return */ @Override public boolean isEnabled() { return true; } }
- 5.登陆接口与退出登陆接口的实现
自定一UserDetailService实现类实现之后接下来就是实现登陆接口与推出登录接口的逻辑了。1.登录接口与退出登陆接口的controller /** * 验证用户名和密码是否正确 * @param admin * @return */ @PostMapping("/admin/login/status") @ApiOperation(value = "验证用户名和密码是否正确", notes = "验证用户名和密码是否正确") @CheckParam public ResponseUtils loginStatus(@RequestBody Admin admin) { String name = admin.getName(); String password = admin.getPassword(); Map<String, String> result = adminService.verifyPassword(name, password); return ResponseUtils.success("登陆成功", result); } @PostMapping("/admin/logout") public ResponseUtils logout() { return adminService.logout(); } 2.登陆接口与推出登录接口的实现类方法 package com.mcj.music.service.impl; import com.mcj.music.domain.LoginUser; import com.mcj.music.service.AdminService; import com.mcj.music.utils.JwtUtil; import com.mcj.music.utils.RedisCache; import com.mcj.music.utils.ResponseUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * @author mcj * @date 2022/10/9 10:27 * @description 用户service实现类 */ @Service public class AdminServiceImpl implements AdminService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; /** * 验证用户名密码是否正确 * * @param username 用户名 * @param password 密码 */ @Override public Map<String, String> verifyPassword(String username, String password) { // AuthenticationManager authenticate进行用户认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 如果认证没通过,给出对应的提示 if (Objects.isNull(authenticate)) { throw new RuntimeException("登陆失败"); } // 如果认证通过了,使用userId生成一个jwt LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String id = loginUser.getAdmin().getId().toString(); String jwt = JwtUtil.createJWT(id); Map<String, String> result = new HashMap<>(); result.put("token", jwt); // 把完整的用户信息存入redis userId作为key redisCache.setCacheObject("token:" + id, loginUser); return result; } /** * 退出登录 * @return */ @Override public ResponseUtils logout() { // 获取SecurityContextHolder中的用户id UsernamePasswordAuthenticationToken authenticationToken = ((UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication()); LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal(); String id = loginUser.getAdmin().getId().toString(); // 删除redis中的值 String redisKey = "token:" + id; redisCache.deleteObject(redisKey); return ResponseUtils.success("注销成功"); } }
- 6.定义jwt登陆请求过滤器
由于用户登陆成功之后会得到一个token,随后后面的访问都会携带这个token,我们需要来通过解析这个token从redis中获取用户的信息,不用每次都访问都要请求一下数据库,这个功能就是在jwt登陆请求过滤器中实现的,会把获取到的用户信息存到SecurityContextHolder中,后面的过滤器会从其中取得用户信息来判断用户是否已通过认证。package com.mcj.music.filter; import com.mcj.music.domain.LoginUser; import com.mcj.music.utils.JwtUtil; import com.mcj.music.utils.RedisCache; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collection; import java.util.Objects; /** * @author mcj * @date 2022/11/20 11:18 * @description jwt登录请求过滤器 */ @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { // 放行 filterChain.doFilter(request, response); return; } // 解析token String userId; try { Claims claims = JwtUtil.parseJWT(token); userId = claims.getSubject(); } catch (Exception e) { throw new RuntimeException("token非法", e); } // 从redis中获取用户信息 String redisKey = "token:" + userId; LoginUser loginUser = redisCache.getCacheObject(redisKey); if (Objects.isNull(loginUser)) { throw new RuntimeException("用户未登录"); } // 存入SecurityContextHolder // 获取权限信息 Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities(); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 放行 filterChain.doFilter(request, response); } }
- 7.授权失败与认证失败处理类
由于我们需要在授权失败和认证失败后返回统一格式的json数据,所以这边需要定义一下这两个授权失败和认证失败的处理类1.授权失败处理类 package com.mcj.music.handler; import com.alibaba.fastjson.JSON; import com.mcj.music.utils.ResponseUtils; import com.mcj.music.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author mcj * @date 2022/11/20 14:06 * @description 用户授权失败处理类 */ @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseUtils result = new ResponseUtils(HttpStatus.FORBIDDEN.value(), "你的权限不足,请联系管理员"); String json = JSON.toJSONString(result); WebUtils.renderString(response, json); } } 2.认证失败处理类 package com.mcj.music.handler; import com.alibaba.fastjson.JSON; import com.mcj.music.utils.ResponseUtils; import com.mcj.music.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author mcj * @date 2022/11/20 14:06 * @description 用户认证失败处理类 */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseUtils result = new ResponseUtils(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登陆"); String json = JSON.toJSONString(result); WebUtils.renderString(response, json); } }
- 8.配置一些接口的访问权限
这里是直接使用@PreAuthorize("hasAuthority('system:test:list')")
这种注解的方式来给接口设置访问权限的。需要注意的是,使用上面的注解需要在启动类上加上@EnableGlobalMethodSecurity(prePostEnabled = true)
开启上面的注解功能。 - 9.写spring security的配置文件
前面的一些工作都已经做完了,然后为了使前面的功能发挥作用,这就需要spring security的配置文件了。package com.mcj.music.config; import com.mcj.music.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * @author mcj * @date 2022/11/20 10:09 * @description spring Security配置类 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; // 创建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http // 关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口允许匿名访问 匿名访问就是未登陆的时候能够访问,已登录的状态不能访问 .antMatchers("/admin/login/status").anonymous() // 除上面外的所有请求都需要鉴权认证 .anyRequest().authenticated(); // 将jwt认证过滤器添加到UsernamePasswordAuthenticationFilter过滤器前面 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 配置异常处理器 http.exceptionHandling() // 认证失败异常处理器 .authenticationEntryPoint(authenticationEntryPoint) // 授权失败认证处理器 .accessDeniedHandler(accessDeniedHandler); // 允许spring security跨域 http.cors(); } }
这里的关闭csrf功能是因为不关闭的话spring security为防范CSRF攻击会自动生成一个csrf_token,前端访问的时候需要携带这个csrf_token,如果没有携带就不允许访问,而前后端分离的项目则是通过携带token来进行认证,则用不到这个csrf_token,所以这边直接关闭就行。
4. 总结
关于spring security前后端分离形式的使用这边就介绍这么多了,主要还是理解spring security的认证过程,然后根据它的认证过程来看我们需要实现什么类,然后在进行类的编写就容易了。另外需要注意一个点就是这边认证过程中的密码比对是加密之后的密码进行比对,所以存入数据库中的密码信息要及逆行加密,这里的加密是通过配置文件中注入的BCryptPasswordEncoder这种方式进行的。
标签:return,String,spring,boot,springframework,org,import,security,public From: https://www.cnblogs.com/mcj123/p/16913515.html