主页:写程序的小王叔叔的博客欢迎来访👀
支持:点赞收藏关注
1、说明
SpringSecurity是一个用于Java 企业级应用程序的安全框架,主要包含用户认证和用户授权两个方面.相比较Shiro而言,Security功能更加的强大,它可以很容易地扩展以满足更多安全控制方面的需求,但也相对它的学习成本会更高,两种框架各有利弊.实际开发中还是要根据业务和项目的需求来决定使用哪一种.
JWT是在Web应用中安全传递信息的规范,从本质上来说是Token的演变,是一种生成加密用户身份信息的Token,特别适用于分布式单点登陆的场景,无需在服务端保存用户的认证信息,而是直接对Token进行校验获取用户信息,使单点登录更为简单灵活.
2、项目环境
SpringBoot版本:2.1.6
SpringSecurity版本: 5.1.5
MyBatis-Plus版本: 3.1.0
JDK版本:1.8
数据表(SQL文件在项目中): 数据库中测试号的密码进行了加密,密码皆为123456:
Maven依赖如下:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><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><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></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></dependencies>
配置如下:
# 配置端口server port8764spring# 配置数据源 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 password123456 type com.alibaba.druid.pool.DruidDataSource # JWT配置jwt# 密匙KEY secret JWTSecret # HeaderKEY tokenHeader Authorization # Token前缀字符 tokenPrefix Sans- # 过期时间 单位秒 1天后过期=86400 7天后过期=604800 expiration86400# 配置不需要认证的接口 antMatchers /index/**,/login/**,/favicon.ico # 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-casetrue# 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏 call-setters-on-nullstrue# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl org.apache.ibatis.logging.stdout.StdOutImpl
3、编写项目基础类
Entity,Dao,Service,及等SpringSecurity用户的Entity,Service类等在这里省略,请参考源码
编写JWT工具类
/*** JWT工具类*/publicclassJWTTokenUtil { publicstaticStringcreateAccessToken(SelfUserEntityselfUserEntity){ // 登陆成功生成JWTStringtoken=Jwts.builder() // 放入用户名和用户ID .setId(selfUserEntity.getUserId()+"") // 主题 .setSubject(selfUserEntity.getUsername()) // 签发时间 .setIssuedAt(newDate()) // 签发者 .setIssuer("sans") // 自定义属性 放入用户拥有权限 .claim("authorities", JSON.toJSONString(selfUserEntity.getAuthorities())) // 失效时间 .setExpiration(newDate(System.currentTimeMillis() +JWTConfig.expiration)) // 签名算法和密钥 .signWith(SignatureAlgorithm.HS512, JWTConfig.secret) .compact(); returntoken; } }
编写暂无权限处理类
publicclassUserAuthAccessDeniedHandlerimplementsAccessDeniedHandler{ publicvoidhandle(HttpServletRequestrequest, HttpServletResponseresponse, AccessDeniedExceptionexception){ ResultUtil.responseJson(response,ResultUtil.resultCode(403,"未授权")); } }
编写登录失败处理类
publicclassUserLoginFailureHandlerimplementsAuthenticationFailureHandler { publicvoidonAuthenticationFailure(HttpServletRequestrequest, HttpServletResponseresponse, AuthenticationExceptionexception){ // 这些对于操作的处理类可以根据不同异常进行不同处理if (exceptioninstanceofUsernameNotFoundException){ log.info("【登录失败】"+exception.getMessage()); ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名不存在")); } if (exceptioninstanceofLockedException){ log.info("【登录失败】"+exception.getMessage()); ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户被冻结")); } if (exceptioninstanceofBadCredentialsException){ log.info("【登录失败】"+exception.getMessage()); ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名密码不正确")); } ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登录失败")); } }
编写登录成功处理类
publicclassUserLoginSuccessHandlerimplementsAuthenticationSuccessHandler { publicvoidonAuthenticationSuccess(HttpServletRequestrequest, HttpServletResponseresponse, Authenticationauthentication){ // 组装JWTSelfUserEntityselfUserEntity= (SelfUserEntity) authentication.getPrincipal(); Stringtoken=JWTTokenUtil.createAccessToken(selfUserEntity); token=JWTConfig.tokenPrefix+token; // 封装返回参数Map<String,Object>resultData=newHashMap<>(); resultData.put("code","200"); resultData.put("msg", "登录成功"); resultData.put("token",token); ResultUtil.responseJson(response,resultData); } }
编写登出成功处理类
publicclassUserLogoutSuccessHandlerimplementsLogoutSuccessHandler { publicvoidonLogoutSuccess(HttpServletRequestrequest, HttpServletResponseresponse, Authenticationauthentication){ Map<String,Object>resultData=newHashMap<>(); resultData.put("code","200"); resultData.put("msg", "登出成功"); SecurityContextHolder.clearContext(); ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData)); } }
4、编写Security核心类
编写自定义登录验证类
publicclassUserAuthenticationProviderimplementsAuthenticationProvider { privateSelfUserDetailsServiceselfUserDetailsService; privateSysUserServicesysUserService; publicAuthenticationauthenticate(Authenticationauthentication) throwsAuthenticationException { // 获取表单输入中返回的用户名StringuserName= (String) authentication.getPrincipal(); // 获取表单中输入的密码Stringpassword= (String) authentication.getCredentials(); // 查询用户是否存在SelfUserEntityuserInfo=selfUserDetailsService.loadUserByUsername(userName); if (userInfo==null) { thrownewUsernameNotFoundException("用户名不存在"); } // 我们还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的if (!newBCryptPasswordEncoder().matches(password, userInfo.getPassword())) { thrownewBadCredentialsException("密码不正确"); } // 还可以加一些其他信息的判断,比如用户账号已停用等判断if (userInfo.getStatus().equals("PROHIBIT")){ thrownewLockedException("该用户已被冻结"); } // 角色集合Set<GrantedAuthority>authorities=newHashSet<>(); // 查询用户角色List<SysRoleEntity>sysRoleEntityList=sysUserService.selectSysRoleByUserId(userInfo.getUserId()); for (SysRoleEntitysysRoleEntity: sysRoleEntityList){ authorities.add(newSimpleGrantedAuthority("ROLE_"+sysRoleEntity.getRoleName())); } userInfo.setAuthorities(authorities); // 进行登录returnnewUsernamePasswordAuthenticationToken(userInfo, password, authorities); } publicbooleansupports(Class<?>authentication) { returntrue; } }
编写自定义PermissionEvaluator注解验证
publicclassUserPermissionEvaluatorimplementsPermissionEvaluator { privateSysUserServicesysUserService; publicbooleanhasPermission(Authenticationauthentication, ObjecttargetUrl, Objectpermission) { // 获取用户信息SelfUserEntityselfUserEntity=(SelfUserEntity) authentication.getPrincipal(); // 查询用户权限(这里可以将权限放入缓存中提升效率)Set<String>permissions=newHashSet<>(); List<SysMenuEntity>sysMenuEntityList=sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId()); for (SysMenuEntitysysMenuEntity:sysMenuEntityList) { permissions.add(sysMenuEntity.getPermission()); } // 权限对比if (permissions.contains(permission.toString())){ returntrue; } returnfalse; } publicbooleanhasPermission(Authenticationauthentication, SerializabletargetId, StringtargetType, Objectpermission) { returnfalse; } }
编写SpringSecurity核心配置类
prePostEnabled=true) //开启权限注解,默认是关闭的 (publicclassSecurityConfigextendsWebSecurityConfigurerAdapter { /*** 自定义登录成功处理器*/privateUserLoginSuccessHandleruserLoginSuccessHandler; /*** 自定义登录失败处理器*/privateUserLoginFailureHandleruserLoginFailureHandler; /*** 自定义注销成功处理器*/privateUserLogoutSuccessHandleruserLogoutSuccessHandler; /*** 自定义暂无权限处理器*/privateUserAuthAccessDeniedHandleruserAuthAccessDeniedHandler; /*** 自定义未登录的处理器*/privateUserAuthenticationEntryPointHandleruserAuthenticationEntryPointHandler; /*** 自定义登录逻辑验证器*/privateUserAuthenticationProvideruserAuthenticationProvider; publicBCryptPasswordEncoderbCryptPasswordEncoder(){ returnnewBCryptPasswordEncoder(); } /*** 注入自定义PermissionEvaluator*/publicDefaultWebSecurityExpressionHandleruserSecurityExpressionHandler(){ DefaultWebSecurityExpressionHandlerhandler=newDefaultWebSecurityExpressionHandler(); handler.setPermissionEvaluator(newUserPermissionEvaluator()); returnhandler; } /*** 配置登录验证逻辑*/protectedvoidconfigure(AuthenticationManagerBuilderauth){ //这里可启用我们自己的登陆验证逻辑auth.authenticationProvider(userAuthenticationProvider); } protectedvoidconfigure(HttpSecurityhttp) throwsException { http.authorizeRequests() //不进行权限验证的请求或资源(从配置文件中读取) .antMatchers(JWTConfig.antMatchers.split(",")).permitAll() //其他的需要登陆后才能访问 .anyRequest().authenticated() .and() //配置未登录自定义处理类 .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler) .and() //配置登录地址 .formLogin() .loginProcessingUrl("/login/userLogin") //配置登录成功自定义处理类 .successHandler(userLoginSuccessHandler) //配置登录失败自定义处理类 .failureHandler(userLoginFailureHandler) .and() //配置登出地址 .logout() .logoutUrl("/login/userLogout") //配置用户登出自定义处理类 .logoutSuccessHandler(userLogoutSuccessHandler) .and() //配置没有权限自定义处理类 .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler) .and() // 开启跨域 .cors() .and() // 取消跨站请求伪造防护 .csrf().disable(); // 基于Token不需要sessionhttp.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用缓存http.headers().cacheControl(); // 添加JWT过滤器http.addFilter(newJWTAuthenticationTokenFilter(authenticationManager())); } }
5、编写JWT拦截类
编写JWT接口请求校验拦截器
/*** JWT接口请求校验拦截器* 请求接口时会进入这里验证Token是否合法和过期* @Author Sans* @CreateTime 2019/10/5 16:41*/publicclassJWTAuthenticationTokenFilterextendsBasicAuthenticationFilter { publicJWTAuthenticationTokenFilter(AuthenticationManagerauthenticationManager) { super(authenticationManager); } protectedvoiddoFilterInternal(HttpServletRequestrequest, HttpServletResponseresponse, FilterChainfilterChain) throwsServletException, IOException { // 获取请求头中JWT的TokenStringtokenHeader=request.getHeader(JWTConfig.tokenHeader); if (null!=tokenHeader&&tokenHeader.startsWith(JWTConfig.tokenPrefix)) { try { // 截取JWT前缀Stringtoken=tokenHeader.replace(JWTConfig.tokenPrefix, ""); // 解析JWTClaimsclaims=Jwts.parser() .setSigningKey(JWTConfig.secret) .parseClaimsJws(token) .getBody(); // 获取用户名Stringusername=claims.getSubject(); StringuserId=claims.getId(); if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) { // 获取角色List<GrantedAuthority>authorities=newArrayList<>(); Stringauthority=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(newSimpleGrantedAuthority(role.get("authority"))); } } } //组装参数SelfUserEntityselfUserEntity=newSelfUserEntity(); selfUserEntity.setUsername(claims.getSubject()); selfUserEntity.setUserId(Long.parseLong(claims.getId())); selfUserEntity.setAuthorities(authorities); UsernamePasswordAuthenticationTokenauthentication=newUsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (ExpiredJwtExceptione){ log.info("Token过期"); } catch (Exceptione) { log.info("Token无效"); } } filterChain.doFilter(request, response); return; } }
6、权限注解和hasPermission权限扩展
Security允许我们在定义URL方法访问所应有的注解权限时使用SpringEL表达式,在定义所需的访问权限时如果对应的表达式返回结果为true则表示拥有对应的权限,反之则没有权限,会进入到我们配置的UserAuthAccessDeniedHandler(暂无权限处理类)中进行处理.这里举一些例子,代码中注释有对应的描述。
表达式
"hasRole('ADMIN')") (value="/info",method=RequestMethod.GET) (publicMap<String,Object>userLogin(){ Map<String,Object>result=newHashMap<>(); SelfUserEntityuserDetails=SecurityUtil.getUserInfo(); result.put("title","管理端信息"); result.put("data",userDetails); returnResultUtil.resultSuccess(result); } "hasAnyRole('ADMIN','USER')") (value="/list",method=RequestMethod.GET) (publicMap<String,Object>list(){ Map<String,Object>result=newHashMap<>(); List<SysUserEntity>sysUserEntityList=sysUserService.list(); result.put("title","拥有用户或者管理员角色都可以查看"); result.put("data",sysUserEntityList); returnResultUtil.resultSuccess(result); } "hasRole('ADMIN') and hasRole('USER')") (value="/menuList",method=RequestMethod.GET) (publicMap<String,Object>menuList(){ Map<String,Object>result=newHashMap<>(); List<SysMenuEntity>sysMenuEntityList=sysMenuService.list(); result.put("title","拥有用户和管理员角色都可以查看"); result.put("data",sysMenuEntityList); returnResultUtil.resultSuccess(result); }
通常情况下使用hasRole和hasAnyRole基本可以满足大部分鉴权需求,但是有时候面对更复杂的场景上述常规表示式无法完成权限认证,Security也为我们提供了解决方案.通过hasPermission()来扩展表达式.使用hasPermission()首先要实现PermissionEvaluator接口
publicclassUserPermissionEvaluatorimplementsPermissionEvaluator { privateSysUserServicesysUserService; publicbooleanhasPermission(Authenticationauthentication, ObjecttargetUrl, Objectpermission) { // 获取用户信息SelfUserEntityselfUserEntity=(SelfUserEntity) authentication.getPrincipal(); // 查询用户权限(这里可以将权限放入缓存中提升效率)Set<String>permissions=newHashSet<>(); List<SysMenuEntity>sysMenuEntityList=sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId()); for (SysMenuEntitysysMenuEntity:sysMenuEntityList) { permissions.add(sysMenuEntity.getPermission()); } // 权限对比if (permissions.contains(permission.toString())){ returntrue; } returnfalse; } publicbooleanhasPermission(Authenticationauthentication, SerializabletargetId, StringtargetType, Objectpermission) { returnfalse; } }
在请求方法上添加hasPermission示例
"hasPermission('/admin/userList','sys:user:info')") (value="/userList",method=RequestMethod.GET) (publicMap<String,Object>userList(){ Map<String,Object>result=newHashMap<>(); List<SysUserEntity>sysUserEntityList=sysUserService.list(); result.put("title","拥有sys:user:info权限都可以查看"); result.put("data",sysUserEntityList); returnResultUtil.resultSuccess(result); }
hasPermission可以也可以和其他表达式联合使用
"hasRole('ADMIN') and hasPermission('/admin/adminRoleList','sys:role:info')") (value="/adminRoleList",method=RequestMethod.GET) (publicMap<String,Object>adminRoleList(){ Map<String,Object>result=newHashMap<>(); List<SysRoleEntity>sysRoleEntityList=sysRoleService.list(); result.put("title","拥有ADMIN角色和sys:role:info权限可以访问"); result.put("data",sysRoleEntityList); returnResultUtil.resultSuccess(result); }
7、测试
创建账户这里用户加密使用了Security推荐的bCryptPasswordEncoder方法
/*** 注册用户*/publicvoidcontextLoads() { // 注册用户SysUserEntitysysUserEntity=newSysUserEntity(); sysUserEntity.setUsername("sans"); sysUserEntity.setPassword(bCryptPasswordEncoder.encode("123456")); // 设置用户状态sysUserEntity.setStatus("NORMAL"); sysUserService.save(sysUserEntity); // 分配角色 1:ADMIN 2:USERSysUserRoleEntitysysUserRoleEntity=newSysUserRoleEntity(); sysUserRoleEntity.setRoleId(2L); sysUserRoleEntity.setUserId(sysUserEntity.getUserId()); sysUserRoleService.save(sysUserRoleEntity); }
登录USER角色账号,登录成功后我们会获取到身份认证的Token
访问USER角色的接口,把上一步获取到的Token设置在Headers中,Key为Authorization,我们之前实现的JWTAuthenticationTokenFilter拦截器会根据请求头中的Authorization获取并解析Token使用USER角色Token访问ADMIN角色的接口,会被拒绝,告知未授权(暂无权限会进入我们定义的UserAuthAccessDeniedHandler这个类进行处理)。更换ADMIN角色进行登录并访问ADMIN接口
⚠️注意 ~
💯本期内容就结束了,如果内容有误,麻烦大家评论区指出!
如有疑问❓可以在评论区💬或私信💬,尽我最大能力🏃♀️帮大家解决👨🏫!
如果我的文章有帮助到您,欢迎点赞+关注✔️鼓励博主🏃,您的鼓励是我分享的动力🏃🏃🏃~