该篇文章,主要基于 Spring 体系下 Security 组件编写的简单案例,结合 Vue 前端对权限功能进行验证
服务端 code
pom 依赖
<!-- spring下组件都依赖于当前parent标签 由它进行版本管理--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> <relativePath/> </parent> <dependencies> <!-- SpringBoot集成web所需依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mysql驱动依赖--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--Security依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- MybatisPlus 核心库 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version> </dependency> <!-- 引入阿里数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency> <!-- StringUtilS工具 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.5</version> </dependency> <!-- JSON工具 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.45</version> </dependency> <!-- JWT依赖 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <!-- thymeleaf模板引擎--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>
application.yml 文件详解
# 配置端口 server: port: 8764 servlet: context-path: / spring: # 配置数据源 数据库稍后有 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/sans_security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false username: root password: myroot type: com.alibaba.druid.pool.DruidDataSource # 该配置最初是在本地实现 security配置是否管用 不需要可不配置 thymeleaf: prefix: classpath:/templates/ check-template-location: true suffix: .html encoding: UTF-8 servlet: content-type: text/html mode: HTML5 cache: false # JWT配置 jwt: # 密匙KEY secret: JWTSecret # HeaderKEY tokenHeader: Authorization # Token前缀字符 tokenPrefix: Security-c # 过期时间 单位秒 1天后过期=86400 7天后过期=604800 expiration: 86400 # 配置不需要认证的接口 antMatchers: /index/**,/login/**,/favicon.ico,*.html,/index,/isPermission # Mybatis-plus相关配置 mybatis-plus: # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置) mapper-locations: classpath:mapper/*.xml # 以下配置均有默认值,可以不设置 global-config: db-config: #主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID"; id-type: AUTO #字段策略 IGNORED:"忽略判断" NOT_NULL:"非 NULL 判断") NOT_EMPTY:"非空判断" field-strategy: NOT_EMPTY #数据库类型 db-type: MYSQL configuration: # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射 map-underscore-to-camel-case: true # 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏 call-setters-on-nulls: true # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
JWTConfig 类
/** * JWT配置类 * @author vnjohn * @since 2022-12-12 **/ @Getter @Component @ConfigurationProperties(prefix = "jwt") public class JWTConfig { /** * 密钥KEY */ public static String secret; /** * TokenKey */ public static String tokenHeader; /** * Token前缀字符 */ public static String tokenPrefix; /** * 过期时间 */ public static Integer expiration; /** * 不需要认证的接口 */ public static String antMatchers; public void setSecret(String secret) { this.secret = secret; } public void setTokenHeader(String tokenHeader) { this.tokenHeader = tokenHeader; } public void setTokenPrefix(String tokenPrefix) { this.tokenPrefix = tokenPrefix; } public void setExpiration(Integer expiration) { this.expiration = expiration * 1000; } public void setAntMatchers(String antMatchers) { this.antMatchers = antMatchers; } }
JWT 工具类
/** * JWT工具类 * @author vnjohn * @since 2022-12-12 **/ @Slf4j public class JWTTokenUtil { /** * 私有化构造器 */ private JWTTokenUtil(){} /** * 生成Token * @author zwq * @date 2020/4/4 * @param selfUserEntity * @return **/ public static String createAccessToken(SelfUserEntity selfUserEntity){ // 登陆成功生成JWT String token = Jwts.builder() // 放入用户名和用户ID .setId(selfUserEntity.getUserId()+"") // 主题 .setSubject(selfUserEntity.getUsername()) // 签发时间 .setIssuedAt(new Date()) // 签发者 .setIssuer("sans") // 自定义属性 放入用户拥有权限 .claim("authorities", JSON.toJSONString(selfUserEntity.getAuthorities())) // 失效时间 .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration)) // 签名算法和密钥 .signWith(SignatureAlgorithm.HS512, JWTConfig.secret) .compact(); return token; } }
SecurityConfig 类
/** * SpringSecurity 配置类 * @author vnjohn * @since 2022-12-12 **/ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的 public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 自定义登录成功处理器 */ @Autowired private UserLoginSuccessHandler userLoginSuccessHandler; /** * 自定义登录失败处理器 */ @Autowired private UserLoginFailureHandler userLoginFailureHandler; /** * 自定义注销成功处理器 */ @Autowired private UserLogoutSuccessHandler userLogoutSuccessHandler; /** * 自定义暂无权限处理器 */ @Autowired private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler; /** * 自定义未登录的处理器 */ @Autowired private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler; /** * 自定义登录逻辑验证器 */ @Autowired private UserAuthenticationProvider userAuthenticationProvider; /** * 加密方式 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } /** * 注入自定义PermissionEvaluator */ @Bean public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){ DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler(); handler.setPermissionEvaluator(new UserPermissionEvaluator()); return handler; } /** * 配置登录验证逻辑 */ @Override protected void configure(AuthenticationManagerBuilder auth){ //这里可启用我们自己的登陆验证逻辑 auth.authenticationProvider(userAuthenticationProvider); } /** * 配置 security 控制逻辑 **/ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 不进行权限验证的请求或资源(从配置文件中读取) .antMatchers(JWTConfig.antMatchers.split(",")).permitAll() // 其他的需要登陆后才能访问 其他url都需要验证 .anyRequest().authenticated() .and() // 配置未登录自定义处理类 .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler) .and() // 配置登录地址 .formLogin() //配置security表单登录页面地址 默认是login .loginPage("/login") //配置security提交form表单请求的接口地址 默认是/login/userLogin .loginProcessingUrl("/myLoginForm") //设置security提交的用户名属性值是那个 默认是username .usernameParameter("userName") //设置security提交的密码属性名是那个 默认是password .passwordParameter("passWord") //.successForwardUrl("/success") 登录成功跳转的请求 集成thymeleaf已经测试过了 //配置登录失败页 //.failureForwardUrl("/error") // 配置登录成功自定义处理类 .successHandler(userLoginSuccessHandler) // 配置登录失败自定义处理类 .failureHandler(userLoginFailureHandler) .and() // 配置登出地址 .logout() .logoutUrl("/login/userLogout") // 配置用户登出自定义处理类 .logoutSuccessHandler(userLogoutSuccessHandler) .and() // 配置没有权限自定义处理类 .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler) .and() // 开启跨域 .cors() .and() // 取消跨站请求伪造防护 .csrf().disable(); // 基于Token不需要session http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用缓存 http.headers().cacheControl(); // 添加JWT过滤器 http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager())); }
SelfUserEntity、SelfUserDetailsService 类
UserDetails 接口、UserDetailsService 接口是 Security 给用户提供的接口,我们需要基于自己的逻辑创建类去实现它
SelfUserEntity 类
/** * SpringSecurity 用户实体 * 注意:这里必须要实现 UserDetails 接口 * @author vnjohn * @since 2022-12-12 **/ @Data public class SelfUserEntity implements Serializable, UserDetails { private static final long serialVersionUID = 1L; /** * 用户ID */ private Long userId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 状态:NORMAL正常 PROHIBIT禁用 */ private String status; /** * 用户角色 */ private Collection<GrantedAuthority> authorities; /** * 账户是否过期 */ private boolean isAccountNonExpired = false; /** * 账户是否被锁定 */ private boolean isAccountNonLocked = false; /** * 证书是否过期 */ private boolean isCredentialsNonExpired = false; /** * 账户是否有效 */ private boolean isEnabled = true; @Override public Collection<GrantedAuthority> getAuthorities() { return authorities; } @Override public boolean isAccountNonExpired() { return isAccountNonExpired; } @Override public boolean isAccountNonLocked() { return isAccountNonLocked; } @Override public boolean isCredentialsNonExpired() { return isCredentialsNonExpired; } @Override public boolean isEnabled() { return isEnabled; } }
SelfUserDetailsService 类
/** * SpringSecurity 用户的业务实现 * 实现 UserDetailsService 接口里面的方法 实现自己的逻辑 * @author vnjohn * @since 2022-12-12 **/ @Component public class SelfUserDetailsService implements UserDetailsService { @Autowired private SysUserService sysUserService; @Override public SelfUserEntity loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 SysUserEntity sysUserEntity =sysUserService.selectUserByName(username); if (sysUserEntity!=null){ // 组装参数 SelfUserEntity selfUserEntity = new SelfUserEntity(); BeanUtils.copyProperties(sysUserEntity,selfUserEntity); return selfUserEntity; } return null; } }
UserAuthAccessDeniedHandler 类
暂无权限处理类,SecurityConfig 配置类已注入
/** * 暂无权限处理类 * @author vnjohn * @since 2022-12-12 **/ @Component public class UserAuthAccessDeniedHandler implements AccessDeniedHandler{ /** * 暂无权限返回结果 **/ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){ ResultUtil.responseJson(response,ResultUtil.resultCode(403,"未授权")); } }
UserAuthenticationEntryPointHandler 类
用户未登录处理类,SecurityConfig 类已注入
/** * 用户未登录处理类 * @author vnjohn * @since 2022-12-12 **/ @Component public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint { /** * 用户未登录返回结果 **/ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){ ResultUtil.responseJson(response,ResultUtil.resultCode(401,"未登录")); } }
UserLoginFailureHandler 类
登录失败处理类,SecurityConfig 类已注入
/** * 登录失败处理类 * @author vnjohn * @since 2022-12-12 **/ @Slf4j @Component public class UserLoginFailureHandler implements AuthenticationFailureHandler { /** * 登录失败返回结果 **/ @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException { // 这些对于操作的处理类可以根据不同异常进行不同处理 if (exception instanceof UsernameNotFoundException){ log.info("【登录失败】"+exception.getMessage()); ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名不存在")); } if (exception instanceof LockedException){ log.info("【登录失败】"+exception.getMessage()); ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户被冻结")); } if (exception instanceof BadCredentialsException){ log.info("【登录失败】"+exception.getMessage()); ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名密码不正确")); } ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登录失败")); } }
UserLoginSuccessHandler 类
登录成功处理类 ,SecurityConfig 类已注入
/** * 登录成功处理类 * @author vnjohn * @since 2022-12-12 **/ @Slf4j @Component public class UserLoginSuccessHandler implements AuthenticationSuccessHandler { /** * 登录成功返回结果 **/ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 组装JWT SelfUserEntity selfUserEntity = (SelfUserEntity) authentication.getPrincipal(); String token = JWTTokenUtil.createAccessToken(selfUserEntity); token = JWTConfig.tokenPrefix + token; // 封装返回参数 Map<String,Object> resultData = new HashMap<>(); resultData.put("code","200"); resultData.put("msg", "登录成功"); resultData.put("token",token); ResultUtil.responseJson(response,resultData); } }
UserLogoutSuccessHandler 类
登出成功处理类,SecurityConfig 类已注入
/** * 登出成功处理类 * @author vnjohn * @since 2022-12-12 **/ @Component public class UserLogoutSuccessHandler implements LogoutSuccessHandler { /** * 用户登出返回结果,这里应该同时让前端清除掉 Token **/ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){ Map<String,Object> resultData = new HashMap<>(); resultData.put("code","200"); resultData.put("msg", "登出成功"); SecurityContextHolder.clearContext(); ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData)); } }
JWTAuthenticationTokenFilter 类
JWT 接口请求校验拦截器,SecurityConfig 类已注入
/** * JWT接口请求校验拦截器 * 请求接口时会进入这里验证 Token 是否合法和过期 * @author vnjohn * @since 2022-12-12 **/ @Slf4j public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter { public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获取请求头中JWT的Token String tokenHeader = request.getHeader(JWTConfig.tokenHeader); if (null!=tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) { try { // 截取JWT前缀 String token = tokenHeader.replace(JWTConfig.tokenPrefix, ""); // 解析JWT Claims claims = Jwts.parser() .setSigningKey(JWTConfig.secret) .parseClaimsJws(token) .getBody(); // 获取用户名 String username = claims.getSubject(); String userId=claims.getId(); if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) { // 获取角色 List<GrantedAuthority> authorities = new ArrayList<>(); String authority = claims.get("authorities").toString(); if(!StringUtils.isEmpty(authority)){ List<Map<String,String>> authorityMap = JSONObject.parseObject(authority, List.class); for(Map<String,String> role : authorityMap){ if(!StringUtils.isEmpty(role)) { authorities.add(new SimpleGrantedAuthority(role.get("authority"))); } } } //组装参数 SelfUserEntity selfUserEntity = new SelfUserEntity(); selfUserEntity.setUsername(claims.getSubject()); selfUserEntity.setUserId(Long.parseLong(claims.getId())); selfUserEntity.setAuthorities(authorities); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (ExpiredJwtException e){ log.info("Token过期"); } catch (Exception e) { log.info("Token无效"); } } filterChain.doFilter(request, response); return; } }
UserAuthenticationProvider 类
自定义登录验证,SecurityConfig 类已注入
/** * 自定义登录验证 * @author vnjohn * @since 2022-12-12 **/ @Component public class UserAuthenticationProvider implements AuthenticationProvider { @Autowired private SelfUserDetailsService selfUserDetailsService; @Autowired private SysUserService sysUserService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 获取表单输入中返回的用户名 String userName = (String) authentication.getPrincipal(); // 获取表单中输入的密码 String password = (String) authentication.getCredentials(); // 查询用户是否存在 SelfUserEntity userInfo = selfUserDetailsService.loadUserByUsername(userName); if (userInfo == null) { throw new UsernameNotFoundException("用户名不存在"); } // 我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的 if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) { throw new BadCredentialsException("密码不正确"); } // 还可以加一些其他信息的判断,比如用户账号已停用等判断 if (userInfo.getStatus().equals("PROHIBIT")){ throw new LockedException("该用户已被冻结"); } // 角色集合 Set<GrantedAuthority> authorities = new HashSet<>(); // 查询用户角色 List<SysRoleEntity> sysRoleEntityList = sysUserService.selectSysRoleByUserId(userInfo.getUserId()); for (SysRoleEntity sysRoleEntity: sysRoleEntityList){ authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName())); } userInfo.setAuthorities(authorities); // 进行登录 return new UsernamePasswordAuthenticationToken(userInfo, password, authorities); } @Override public boolean supports(Class<?> authentication) { return true; } }
UserPermissionEvaluator 类
自定义权限注解验证,SecurityConfig 类已注入
/** * 自定义权限注解验证 * @author vnjohn * @since 2022-12-12 **/ @Component public class UserPermissionEvaluator implements PermissionEvaluator { @Autowired private SysUserService sysUserService; /** * hasPermission 鉴权方法 * 这里仅仅判断 PreAuthorize 注解中的权限表达式 * 实际中可以根据业务需求设计数据库通过 targetUrl 和 permission 做更复杂鉴权 * 当然 targetUrl 不一定是URL可以是数据Id还可以是管理员标识等,这里根据需求自行设计 **/ @Override public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) { // 获取用户信息 SelfUserEntity selfUserEntity =(SelfUserEntity) authentication.getPrincipal(); // 查询用户权限(这里可以将权限放入缓存中提升效率) Set<String> permissions = new HashSet<>(); List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId()); for (SysMenuEntity sysMenuEntity:sysMenuEntityList) { permissions.add(sysMenuEntity.getPermission()); } // 权限对比 if (permissions.contains(permission.toString())){ return true; } return false; } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { return false; } }
以上的 handler、jwtFilter 来说,都是为了实现登录流程,登录时需要走我们对应登录认证handler(UserAuthenticationProvider类),等我们方法上有标识 hasPermission注解时会走我们对应的权限认证handler(UserPermissionEvaluator类):
如下: