SpringSecurity前后端分离
实现的具体思路:
要实现通过Token令牌来认证信息的访问权限
- 通过redis来保存用户信息,首先我们需要登录,如果账号和密码都正确的话,我们将用户类保存到redis内存中去
- 然后我们访问API时安排是否存在Token,如果存在token,则将token解密,token里边保存的是一个随机生成的唯一ID,我们获取这个ID后在redis中找出对应的redis存储的数据,最后转换成为用户数据就可以了
在实现过程中,要实现JWT认证最重要的是,要去掉session
,然后添加FilterSecurityInterceptor 拦截器即可,如果其他信息都正确就放行
一、快速入门
- 搭建SpringBoot项目
- 在第10章的pom.xml中新增
JWT
项,并创建SecurityApplication
运行类
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.8</version>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
- 新增配置
application.yml
token:
header: Authorization
# 令牌前缀
token-start-with: abc
# 使用Base64对该令牌进行编码
base64-secret: 123123123
# 令牌过期时间 此处单位/毫秒
token-validity-in-seconds: 14400000
- 放入常用的
ResdisCache
操作模板
/**
* spring redis 工具类
* @author Community
**/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
* @param key 缓存的键值
* @param value 缓存的值
*/
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 时间颗粒度
*/
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 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
* @param key
*/
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数据
* @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 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
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键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 获取多个Hash中的数据
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
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);
}
}
- 数据库和10的一样,将对应的
SysUser、Permission
复制到model包下,和其相应的mapper类等 - 模拟一个用户请求的控制类,和10一样就好
- 从10里边复制几个权限控制类
-
CustomizeAbstractSecurityInterceptor
拦截器 ,拦截全部请求 -
CustomizeAccessDecisionManager
访问决策管理器 -
CustomizeFilterInvocationSecurityMetadataSource
获取所有权限,并判断是否进行拦截 -
CustomizeAccessDeniedHandler
定义【登录后】无权访问,返回JOSN数据 -
CustomizeAuthenticationEntryPoint
登录失败
二、继承UserDetails
接下来是实现JWT,前后端分离,通过token认证的【重点】
- 在模型类里边添加LoginUser,重写权限认证的**
UserDetails
**接口
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**用户唯一标识*/
private String token;
/*登录时间*/
private Long loginTime;
/** 过期时间*/
private Long expireTime;
/** 登录地点*/
private String loginLocation;
/** 浏览器类型*/
private String browser;
/** 操作系统*/
private String os;
/** 权限列表*/
private Set<GrantedAuthority> authorities;
/** 用户信息*/
private SysUser user;
private List<GrantedAuthority> grantedAuthorities;
public LoginUser(SysUser user, Collection<? extends GrantedAuthority> authorities) {this.user = user;this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new LoginUser.AuthorityComparator());Iterator var2 = authorities.iterator();while (var2.hasNext()) {GrantedAuthority grantedAuthority = (GrantedAuthority) var2.next();Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");sortedAuthorities.add(grantedAuthority);}return sortedAuthorities;
}
/** 参考内置user实现的,必须的*/
private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {private static final long serialVersionUID = 510L;private AuthorityComparator() {}public int compare(GrantedAuthority g1, GrantedAuthority g2) {if (g2.getAuthority() == null) {return -1;} else {return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority());}}
}
@JsonIgnore
@Override
public String getPassword() {return user.getPassword();
}
@Override
public String getUsername() {return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JsonIgnore
@Override
public boolean isAccountNonExpired() {return user.getAccountNonExpired();
}
/*** 指定用户是否解锁,锁定的用户无法进行身份验证* @return boolean*/
@JsonIgnore
@Override
public boolean isAccountNonLocked() {return user.getAccountNonLocked();
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
* @return boolean
*/
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {return user.getCredentialsNonExpired();
}
/**
* 是否可用 ,禁用的用户不能身份验证
* @return boolean
*/
@JsonIgnore
@Override
public boolean isEnabled() {return user.getEnabled();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;
}
}
- 创建一个配置类
SecurityProperties
用于引入,application.yml
的配置
@Data
@Component
@ConfigurationProperties(prefix = "token")
public class SecurityProperties {
/** Request Headers :Authorization */
private String header;
/** 令牌前缀,最后留个空格 Bearer */
private String tokenStartWith;
/** Base64对该令牌进行编码 */
private String base64Secret;
/** 令牌过期时间 此处单位/毫秒 */
private Integer tokenValidityInSeconds;
}
- 创建一个结合redis工具类,创建和保存token的类
TestToken
@Component
@Service
@AllArgsConstructor
public class TestToken {
private SecurityProperties jwtSecurity;
public static final String LOGIN_USER_KEY = "login_user_key";
private RedisCache redisCache;
private HttpServletRequest request;
/**
* 根据用户登录类生成指定的token信息并返回
* @param loginUser
* @return
*/
public String createToken(LoginUser loginUser) {
String token = IdUtil.fastUUID();
loginUser.setToken(token);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(LOGIN_USER_KEY, token);
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, jwtSecurity.getBase64Secret()).compact();
}
// 更新token在 redis里边的缓存
public void refreshToken(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + jwtSecurity.getTokenValidityInSeconds());
// 根据uuid将loginUser缓存
redisCache.setCacheObject(loginUser.getToken(), loginUser, jwtSecurity.getTokenValidityInSeconds(), TimeUnit.MINUTES);
}
// 将token 封装成为LoginUser类
public LoginUser getLoginUser() {
// 获取请求携带的令牌
String token = getToken(request);
try {
if (Validator.isNotEmpty(token)) {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(LOGIN_USER_KEY);
System.out.println(redisCache.getCacheObject(uuid).toString());
return redisCache.getCacheObject(uuid);
}
} catch (Exception e) {
return null;
}
return null;
}
// 将token解密成为明文
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecurity.getBase64Secret())
.parseClaimsJws(token)
.getBody();
}
// 从请求头中获取相应的token,如果有前缀去掉前缀
private String getToken(HttpServletRequest request) {
String token = request.getHeader(jwtSecurity.getHeader());
if (Validator.isNotEmpty(token) && token.startsWith(jwtSecurity.getTokenStartWith())) {
token = token.replace(jwtSecurity.getTokenStartWith(), "");
}
return token;
}
/*判断是否超时,没有超时则刷新*/
public void verifyToken(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= 20 * 60 * 1000L) {
refreshToken(loginUser);
}
}
}
- 写UserDetailsService实现类,该类主要用于登录信息验证,和权限验证
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private PermissionMapper permissionDao;
private UserMapper userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户
LambdaQueryWrapper<SysUser> wappers = Wrappers.lambdaQuery();
wappers.eq(SysUser::getAccount, username); // 模糊查询
SysUser sysUser = userDao.selectOne(wappers);
if (sysUser == null) {
return null;
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//获取该用户所拥有的权限
List<Permission> sysPermissions = permissionDao.selectListByUser(sysUser.getId().toString());
sysPermissions.forEach(sysPermission -> {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode());
grantedAuthorities.add(grantedAuthority);
System.out.println(sysPermission.getPermissionCode());
});
return createLoginUser(sysUser,grantedAuthorities);
}
public UserDetails createLoginUser(SysUser user, List<GrantedAuthority> grantedAuthorities) {
return new LoginUser(user, grantedAuthorities);
}
}
- 写登录实现类,即自定义登录接口的服务类;【登录的入口】
@Component
public class LoginService {
@Autowired
private TestToken tokenService;
@Resource
private AuthenticationManager authenticationManager;
/**
* 登录验证
* @param username 用户名
* @param password 密码
* @return 结果
*/
public R login(String username, String password) throws Exception {
// 用户验证
R result = null;
Authentication authentication = null;
try {
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
//密码错误
result = R.failed("密码错误");
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = R.failed("密码过期");
} else if (e instanceof DisabledException) {
//账号不可用
result = R.failed("账号不可用");
} else if (e instanceof LockedException) {
//账号锁定
result = R.failed("账号锁定");
} else if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
result = R.failed("用户不存在");
} else {
//其他错误
result = R.failed("其他错误");
}
return result;
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return R.ok(tokenService.createToken(loginUser));
}
}
- 在controller类中调用6点方法
private LoginService loginService;
@PostMapping("/login")
public R<Object> login(String username,String password) throws Exception {
return loginService.login(username, password);
}
- 写
CorsFilter
类跨域请求的配置类
@Configuration
public class ResourcesConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/** swagger配置 */
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOrigin("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 对接口配置跨域设置
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
- 最后边的全局配置类
@Configuration
@EnableWebSecurity
@AllArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解权限
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private CustomizeAuthenticationEntryPoint authenticationEntryPoint;
private UserDetailsService userDetailsService;
private CustomizeAbstractSecurityInterceptor securityInterceptor;
private JwtAuthenticationTokenFilter authenticationTokenFilter;
private CorsFilter corsFilter;
private CustomizeAccessDeniedHandler accessDeniedHandler;
// userDetailsService自定义用户登录权限配置 BCryptPasswordEncoder加密密码
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF禁用,因为不使用session
http.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous()
.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
.antMatchers("/profile/**").anonymous()
.antMatchers("/common/download**").anonymous()
.antMatchers("/common/download/resource**").anonymous()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
.antMatchers("/api/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable()
.and().exceptionHandling().
accessDeniedHandler(accessDeniedHandler)//权限不足时的提示信息【登录成功后】
;
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
// 需要这个来过滤掉有token的连接,就是放行有token的请求
http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
http.addFilterBefore(corsFilter, LogoutFilter.class);
}
}
- 自定义PreAuthorize的执行服务【用于做权限管理】
@Service("cc")
public class CCPermissionService {
/**
* 所有权限标识
*/
private static final String ALL_PERMISSION = "*:*:*";
/**
* 管理员角色权限标识
*/
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMETER = ",";
private static final String PERMISSION_DELIMETER = ",";
@Autowired
private TestToken tokenService;
/**
* 验证用户是否具备某权限
*
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
public boolean hasPermi(String permission) {
if (Validator.isEmpty(permission)) {
return false;
}
LoginUser loginUser = tokenService.getLoginUser();
if (Validator.isNull(loginUser) || Validator.isEmpty(loginUser.getAuthorities())) {
return false;
}
return hasPermissions(loginUser.getAuthorities(), permission);
}
/**
* 验证用户是否不具备某权限,与 hasPermi逻辑相反
*
* @param permission 权限字符串
* @return 用户是否不具备某权限
*/
public boolean lacksPermi(String permission) {
return hasPermi(permission) != true;
}
/**
* 验证用户是否具有以下任意一个权限
*
* @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
* @return 用户是否具有以下任意一个权限
*/
public boolean hasAnyPermi(String permissions) {
if (Validator.isEmpty(permissions)) {
return false;
}
LoginUser loginUser = tokenService.getLoginUser();
if (Validator.isNull(loginUser) || Validator.isEmpty(loginUser.getAuthorities())) {
return false;
}
Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();
for (String permission : permissions.split(PERMISSION_DELIMETER)) {
if (permission != null && hasPermissions(authorities, permission)) {
return true;
}
}
return false;
}
/**
* 判断是否包含权限
*
* @param permissions 权限列表
* @param permission 权限字符串
* @return 用户是否具备某权限
*/
private boolean hasPermissions(Collection<? extends GrantedAuthority> permissions, String permission) {
return permissions.contains(new SimpleGrantedAuthority(ALL_PERMISSION)) || permissions.contains(new SimpleGrantedAuthority(StrUtil.trim(permission)));
}
}
- 最后在控制类中添加注解即可
@GetMapping("/getUser")
@PreAuthorize("@cc.hasPermi('query_user1')")
public R getUser() {
return R.ok(testToken.getLoginUser());
}
在这个过程中遇到的问题
- 可就个方面都配置好了,token可以获取成功,其他的没有报错,但是就是访问不了对应的接口,问题的可以原因是
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 类最后的认证出现了问题,权限没给足,或者相关类没配置好