SpringSecurity前后端分离

实现的具体思路:

要实现通过Token令牌来认证信息的访问权限

  • 通过redis来保存用户信息,首先我们需要登录,如果账号和密码都正确的话,我们将用户类保存到redis内存中去
  • 然后我们访问API时安排是否存在Token,如果存在token,则将token解密,token里边保存的是一个随机生成的唯一ID,我们获取这个ID后在redis中找出对应的redis存储的数据,最后转换成为用户数据就可以了

在实现过程中,要实现JWT认证最重要的是,要去掉session,然后添加FilterSecurityInterceptor 拦截器即可,如果其他信息都正确就放行

一、快速入门

  1. 搭建SpringBoot项目
  2. 在第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>
  1. 新增配置 application.yml
token:
  header: Authorization
  # 令牌前缀
  token-start-with: abc
  # 使用Base64对该令牌进行编码
  base64-secret: 123123123
  # 令牌过期时间 此处单位/毫秒
  token-validity-in-seconds: 14400000
  1. 放入常用的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);
    }
}
  1. 数据库和10的一样,将对应的SysUser、Permission复制到model包下,和其相应的mapper类等
  2. 模拟一个用户请求的控制类,和10一样就好
  3. 从10里边复制几个权限控制类
  • CustomizeAbstractSecurityInterceptor 拦截器 ,拦截全部请求
  • CustomizeAccessDecisionManager访问决策管理器
  • CustomizeFilterInvocationSecurityMetadataSource 获取所有权限,并判断是否进行拦截
  • CustomizeAccessDeniedHandler定义【登录后】无权访问,返回JOSN数据
  • CustomizeAuthenticationEntryPoint 登录失败

二、继承UserDetails

接下来是实现JWT,前后端分离,通过token认证的【重点】


  1. 在模型类里边添加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;
    }
}
  1. 创建一个配置类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;

}
  1. 创建一个结合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);
        }
    }
}
  1. 写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);
    }
}
  1. 写登录实现类,即自定义登录接口的服务类;【登录的入口】
@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));
    }
}
  1. 在controller类中调用6点方法
private LoginService loginService;
	@PostMapping("/login")
    public R<Object> login(String username,String password) throws Exception {
        return loginService.login(username, password);
    }
  1. 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);
    }
}
  1. 最后边的全局配置类
@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);
    }
}
  1. 自定义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)));
    }
}
  1. 最后在控制类中添加注解即可
@GetMapping("/getUser")
    @PreAuthorize("@cc.hasPermi('query_user1')")
    public R getUser() {
        return R.ok(testToken.getLoginUser());
    }

在这个过程中遇到的问题

  1. 可就个方面都配置好了,token可以获取成功,其他的没有报错,但是就是访问不了对应的接口,问题的可以原因是
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException  类最后的认证出现了问题,权限没给足,或者相关类没配置好