一、概念:
SpringSecurity是Spring家族中的一个安全管理框架,它的底层是一系列的拦截器和拦截规则,相当于一个拦截器链
二、核心作用:
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
三、核心流程图:
四、核心配置接口:WebSecurityConfigurerAdapter
在项目中使用一般定义一个配置类实现该接口,然后即可通过重新其中的配置方法对SpringSecurity相关规则进行配置,示例代码如下:
/** * spring security配置 * * */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/** * 自定义用户认证逻辑 */
@Autowired
private UserDetailsService userDetailsService;
/** * 认证失败处理类 */
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/** * 退出处理类 */
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/** * token认证过滤器 */
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/** * 跨域过滤器 */
@Autowired
private CorsFilter corsFilter;
/** * 解决 无法直接注入 AuthenticationManager
* * @return
* @throws Exception */
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/** * anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问 */
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity // CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/*.ttf" ).permitAll().
antMatchers(
//mybatis复习相关的接口全部放行,同学们可以通过postMan进行测试而不需要进行权限认证
"/review/**", "/review" ).permitAll()
.antMatchers("/common/downloadByMinio**").permitAll()
.antMatchers("/profile/**").anonymous()
.antMatchers("/common/download**").anonymous()
.antMatchers("/common/download/resource**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/** * 强散列哈希加密实现 */
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/** * 身份认证接口 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
五、认证流程:
1、SpringSecurity体系中不包含JWT的认证,默认使用session认证,如果需要使用token认证那么需要自行定义token解析用的过滤器,进行token验证,并在配置类中关闭拦截器的session认证,将token拦截器添加到拦截器链中
2、自定义token验证过滤器
1)、通过request进行token解析
2)、若token不存在则表示当前用户未登录,直接放行(后续由其他拦截器进行拦截,在上述配置中有体现)
3)、若token存在则进行token解析,将解析的结果封装成LoginUser(用户登录的实体类含用户权限信息)进行返回,并存储到Redis中
4)、若用户已登录则将用户及其权限信息存入SecurityContextHolder中方便后续的拦截器调用
PS:SecurityContextHolder是SpringSecurity最基本的组件了,是用来存放SecurityContext的对象,默认是使用ThreadLocal实现的,这样就保证了本线程内所有的方法都可以获得SecurityContext对象
//解析token的相关代码
public LoginUser getLoginUser(HttpServletRequest request) {
// 获取请求携带的令牌 String token = getToken(request);
if (StringUtils.isNotEmpty(token)) {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
return null;
}
@Componentpublic
class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
LoginUser loginUser = tokenService.getLoginUser(request);//从redis中获取登录用户的信息
//若loginUser不为null则表示当前用户已登录 将用户和用户权限信息存入到SecurityContextHolder中,方便后续的取用
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
2、若用户未登录,则进入登录接口(Jwt令牌的解析在登录确认前,在上述配置类中已配置)
1)、先清除Redis中的验证码数据,防止数据入侵
2)、验证验证码的正确性
3)、通过authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password))
方法验证账户密码,此方法底层会调用UserDetailsService接口实现类的 loadUserByUsername方法来验证用户名和密码,这个实现类和方法需要自定义。
4)、若上述验证正确,则会返回一个LoginUser对象并存入authentication对象的Principal属性中,再将Principal属性在登录服务中(SysLoginService)进行强转成LoginUser,并以此生成token返回给Controller层,由Controller进行封装返回给前端
5)、至此使用SpringSecurity进行用户的登录验证功能就完成,下次用户进行访问时会走Jwt令牌解析,进行权限的访问等相关验证
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new BaseException("对不起,您的账号:" + username + " 已被删除");
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new BaseException("对不起,您的账号:" + username + " 已停用");
}
return createLoginUser(user);
}
@Componentpublic
class SysLoginService {
@Autowired
private TokenService tokenService;//token解析和生成工具类
@Resource
private AuthenticationManager authenticationManager;//SpringSecurity中提供的认证管理器
@Autowired
private RedisCache redisCache;//redis操作工具类
/** * 登录验证 * * @param username 用户名 * @param password 密码 * @param code 验证码 * @param uuid 唯一标识 * @return 结果 */
public String login(String username, String password, String code, String uuid) {
//删除redis中登录用的验证码
String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
String captcha = redisCache.getCacheObject(verifyKey);
redisCache.deleteObject(verifyKey);
//登录错误判断及日志存储
if (captcha == null)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
throw new CaptchaExpireException();
}
if (!code.equalsIgnoreCase(captcha))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
throw new CaptchaException();
}
// 用户验证
Authentication authentication = null;
try {
/** * !!!看我看我看我!!!
* 看我少走弯路,获取用户对象的时候,会去调用下面的这个方法查询用户对象
* UserDetailsServiceImpl.loadUserByUsername */
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
System.out.println("username "+username+" -----password "+password);
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}catch (Exception e) {
e.printStackTrace();
if (e instanceof BadCredentialsException) {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}else {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new CustomException(e.getMessage());
}
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
}
六、鉴权流程:
上述经过登录验证流程后,进入系统SpringSecurity鉴权流程,其中鉴权的核心是FilterSecurityInterceptor拦截器,搭配@PreAuthorize注解实现接口权限控制
鉴权流程:
在登录验证的过程中,会将用户的信息会存储在缓存和SecurityContextHolder中,在鉴权时即可拿出来,与Controller层中接口的方法上方的注解方法进行比对,若符合则放行,若不符则直接返回提示消息给前端
PS:关于前端模块的显隐也是由SpringSecurity和前端框架结合进行控制,核心jar时Thymeleaf
示例:
@PreAuthorize("@ss.hasPermi('clues:clue:false')")
@Log(title = "上传线索", businessType = BusinessType.UPDATE)
@PutMapping("/false/{id}")
public AjaxResult falseClue(@PathVariable String id, @RequestBody FalseClueDTO falseClueDTO){
其中‘clues:clue:false’代表当前接口访问所需的权限,@ss是自定义的一个鉴权方法,代码如下:
package com.huike.framework.web.service;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import com.huike.common.core.domain.entity.SysRole;
import com.huike.common.core.domain.model.LoginUser;
import com.huike.common.utils.ServletUtils;
import com.huike.common.utils.StringUtils;
/** * 自定义权限实现,ss取自SpringSecurity首字母 * @author wgl */@Service("ss")
public class PermissionService {
/** 所有权限标识 */
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 TokenService tokenService;
/** * 验证用户是否具备某权限 * * @param permission 权限字符串 * @return 用户是否具备某权限 */ public boolean hasPermi(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return hasPermissions(loginUser.getPermissions(), 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 (StringUtils.isEmpty(permissions))
{
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
{
return false;
}
Set<String> authorities = loginUser.getPermissions();
for (String permission : permissions.split(PERMISSION_DELIMETER))
{
if (permission != null && hasPermissions(authorities, permission))
{
return true;
}
}
return false;
}
/** * 判断用户是否拥有某个角色 * * @param role 角色字符串 * @return 用户是否具备某角色 */
public boolean hasRole(String role)
{
if (StringUtils.isEmpty(role))
{
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
{
return false;
}
for (SysRole sysRole : loginUser.getUser().getRoles())
{
String roleKey = sysRole.getRoleKey();
if (SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))
{
return true;
}
}
return false;
}
/** * 验证用户是否不具备某角色,与 isRole逻辑相反。 * * @param role 角色名称 * @return 用户是否不具备某角色 */
public boolean lacksRole(String role)
{
return hasRole(role) != true;
}
/** * 验证用户是否具有以下任意一个角色 * * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表 * @return 用户是否具有以下任意一个角色 */
public boolean hasAnyRoles(String roles)
{
if (StringUtils.isEmpty(roles))
{
return false;
}
LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
{
return false;
}
for (String role : roles.split(ROLE_DELIMETER))
{
if (hasRole(role))
{
return true;
}
}
return false;
}
/** * 判断是否包含权限 * * @param permissions 权限列表 * @param permission 权限字符串 * @return 用户是否具备某权限 */
private boolean hasPermissions(Set<String> permissions, String permission)
{
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
}