SpringSecurity5.X权限管理以及OAuth2.0统一认证
SpringSecurity简单介绍
Spring Security 是 Spring 社区的一个顶级项目,也是 Spring Boot 官方推荐使用的安全框架。除了常规的认证(Authentication)和授权(Authorization)之外,Spring Security还提供了诸如ACLs,LDAP,JAAS,CAS等高级特性以满足复杂场景下的安全需求。另外,就目前而言,Spring Security和Shiro也是当前广大应用使用比较广泛的两个安全框架。
spring security 的核心功能主要包括:
- 认证 (你是谁)
- 授权 (你能干什么)
- 攻击防护 (防止伪造身份)
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。
比如,对于username password认证过滤器来说,会检查是否是一个登录请求;是否包含username 和 password (也就是该过滤器需要的一些认证信息) ;如果不满足则放行给下一个。 下一个按照自身职责判定是否是自身需要的信息,basic的特征就是在请求头中有 Authorization:Basic Auth 的信息。中间可能还有更多的认证过滤器。
最后一环是 FilterSecurityInterceptor,这里会判定该请求是否能进行访问rest服务,判断的依据是 BrowserSecurityConfig中的配置,如果被拒绝了就会抛出不同的异常(根据具体的原因)。Exception Translation Filter 会捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示。
注意:绿色的过滤器可以配置是否生效,其他的都不能控制。
SpringSecurity应用说明
-
准备工作介绍
-
技术环境
SpringBoot 2.x + SpringSecurity 5.0.6 + SpringJPA + Druid + mySQL + Thymeleaf + Lomok
- 依赖引入
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
- 文件结构
└─com └─example └─securityjpa │ SecurityJpaApplication.java │ ├─config │ DruidConfig.java Druid配置文件 │ SecurityConfig.java SpringSecurity配置文件 │ ├─controller │ CommonController.java 页面映射 │ ├─entity │ RoleEntity.java 角色类型 │ UserEntity.java 用户类型 │ ├─handler │ CustomAccessDeniedHandler.java 自定义授权失败 │ myPassWordEncoder.java 自定义密码加密规则 │ ├─jpa │ UserEntityJpa.java JPA连接类 │ └─service UserService.java 自定义用户用户认证规则
-
-
定义用户UserEntity
/** * 用户类 * 在SpringSecurity中,用户表对象类需要实现UserDetails接口 * * @author BaoZhou * @date 2018/7/4 */ @Entity @Table(name = "users") @Getter @Setter public class UserEntity implements Serializable, UserDetails { @Id @Column(name = "u_id") private Long id; @Column(name = "u_username") private String username; @Column(name = "u_password") private String password; @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "user_roles", joinColumns = { @JoinColumn(name = "ur_user_id") }, inverseJoinColumns = { @JoinColumn(name = "ur_role_id") } ) private List<RoleEntity> roles; //设置用户身份权限 @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); List<RoleEntity> roles = getRoles(); for (RoleEntity role : roles) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } //设置用户密码 @Override public String getPassword() { return password; } //设置用户名 @Override public String getUsername() { return username; } //设置账户没有过期 @Override public boolean isAccountNonExpired() { return true; } //设置账户没有被锁 @Override public boolean isAccountNonLocked() { return true; } //设置认证没有过期 @Override public boolean isCredentialsNonExpired() { return true; } //设置用户可以用 @Override public boolean isEnabled() { return true; }
- 定义UserService
/** - 用户信息JPA,JPA中只要按照命名规则,JPA会自动配置查找方法,无需实现 - @author BaoZhou - @date 2018/7/4 */ public interface UserEntityJpa extends JpaRepository<UserEntity,Long> { public UserEntity findByUsername (String username); }
/** * 自定义身份认证 * 需要实现UserDetailsService接口 * @author BaoZhou * @date 2018/7/4 */ public class UserService implements UserDetailsService { @Autowired UserEntityJpa userEntityJpa; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserEntity userEntity = userEntityJpa.findByUsername(username); if (userEntity == null) { throw new UsernameNotFoundException("未找到用户名"); } return userEntity; } }
- SecurityConfig配置类
/** * @author: BaoZhou * @date : 2018/7/4 17:15 */ @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean UserDetailsService getServiceDetail() { return new UserService(); } //SpringSecurity会默认在身份前加上前缀,这里设置去除 @Override public void configure(WebSecurity web) throws Exception { web.expressionHandler(new DefaultWebSecurityExpressionHandler() { @Override protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) { WebSecurityExpressionRoot root = (WebSecurityExpressionRoot) super.createSecurityExpressionRoot(authentication, fi); //remove the prefix ROLE_ root.setDefaultRolePrefix(""); return root; } }); } //SpringSecurity5.0.6必须使用加密算法,此处注入加密方法 @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 注入自定义认证类 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(getServiceDetail()); } @Override protected void configure(HttpSecurity http) throws Exception { /*匹配所有路径的*/ http /*关闭跨站支持,不关闭的话,无法登陆Druid监控页面*/ .csrf() .disable() .authorizeRequests() .antMatchers("/").permitAll() ... ... ... } }
-
配置文件
//配置文件中主要是Druid与JPA的配置 spring: datasource: username: root password: 123456 url: jdbc:mysql://192.168.15.128:3306/jdbc driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: {stat,wall,log4j} maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 jpa: show-sql: true hibernate: ddl-auto: update server: port: 8005
OAuth 2.0介绍
OAuth 2.0授权框架支持第三方支持访问有限的HTTP服务,通过在资源所有者和HTTP服务之间进行一个批准交互来代表资源者去访问这些资源,或者通过允许第三方应用程序以自己的名义获取访问权限。
第三方应用可以在资源所有者(用户)批准后,去OAuth2.0申请资源访问token,随后依据token去资源服务器获取相关资源文件.
资源所有者(用户)批准可能需要OAuth2.0参与来保证安全性.我们常用微信授权是在微信上完成的,
可以联想一下微信公众平台开发,在微信公众平台开发过程中当我们访问某个页面,页面可能弹出一个提示框应用需要获取我们的个人信息问是否允许,点确认其实就是授权第三方应用获取我们在微信公众平台的个人信息。这里微信网页授权就是使用的OAuth2.0。
client-server认证模型
在传统的client-server认证模型中,客户端通过提供资源所有者的凭证来请求服务器访问一个受限制的资源(受保护的资源)。为了让第三方应用可以访问这些受限制的资源,资源所有者共享他的凭证给第三方应用。
OAuth定义了四种角色:
-
resource owner(资源所有者)
-
resource server(资源服务器)
-
client(客户端):代表资源所有者并且经过所有者授权去访问受保护的资源的应用程序
- authorization server(授权服务器):在成功验证资源所有者并获得授权后向客户端发出访问令牌
常规OAuth2.0流程
抽象的OAuth2.0流程如图所示:
- (A) 客户端向资源所有者请求其授权
- (B) 客户端收到资源所有者的授权许可,这个授权许可是一个代表资源所有者授权的凭据
- (C) 客户端向授权服务器请求访问令牌,并出示授权许可
- (D) 授权服务器对客户端身份进行认证,并校验授权许可,如果都是有效的,则发放访问令牌
- (E) 客户端向资源服务器请求受保护的资源,并出示访问令牌
- (F) 资源服务器校验访问令牌,如果令牌有效,则提供服务
Refresh Token流程
Refresh Token是用于获取Access Token的凭据。刷新令牌是授权服务器发给客户端的,用于在当前访问令牌已经失效或者过期的时候获取新的访问令牌。刷新令牌只用于授权服务器,并且从来不会发给资源所有者。
刷新的流程如图所示:
- (A) 客户端请求获取访问令牌,并向授权服务器提供授权许可
- (B) 授权服务器对客户端身份进行认证,并校验授权许可,如果校验通过,则发放访问令牌和刷新令牌
- (C) 客户端访问受保护的资源,并向资源服务器提供访问令牌
- (D) 资源服务器校验访问令牌,如果校验通过,则提供服务
- (E) 重复(C)和(D)直到访问令牌过期。如果客户端直到访问令牌已经过期,则跳至(G),否则不能继续访问受保护的资源
- (F) 自从访问令牌失效以后,资源服务器返回一个无效的令牌错误
- (G) 客户端请求获取一个新的访问令牌,并提供刷新令牌
- (H) 授权服务器对客户端进行身份认证并校验刷新令牌,如果校验通过,则发放新的访问令牌(并且,可选的发放新的刷新令牌)
Client Registration客户端注册模型
在使用该协议之前,客户端向授权服务器注册。
OAuth定义了两种客户端类型:
- confidential:能够维护其凭证的机密性的客户端
- public:不能维护其凭证的机密性的客户端
授权处理用两个授权服务器端点:
- Authorization endpoint:用于客户端从资源所有者那里获取授权
-
Token endpoint:用于客户端用授权许可交互访问令牌
- Redirection endpoint:用于资源服务器通过资源所有者用户代理将包含授权凭据的响应返回给客户端
为了获得一个访问令牌,客户端需要先从资源所有者那里获得授权。授权是以授权许可的形式来表示的。
OAuth定义了四种授权类型:
- authorization code - 授权码
- implicit - 隐式授权
- resource owner password credentials - 资源所有者密码凭证授予
- client credentials - 客户端凭证
授权码
授权码流程如图所示:
- (A) 客户端通过将资源所有者的用户代理指向授权端点来启动这个流程。客户端包含它的客户端标识符,请求范围,本地状态,和重定向URI,在访问被允许(或者拒绝)后授权服务器立即将用户代理返回给重定向URI。
- (B) 授权服务器验证资源所有者(通过用户代理),并确定资源所有者是否授予或拒绝客户端的访问请求。
- (C) 假设资源所有者授权访问,那么授权服务器用之前提供的重定向URI(在请求中或在客户端时提供的)将用户代理重定向回客户端。重定向URI包括授权码和前面客户端提供的任意本地状态。
- (D) 客户端用上一步接收到的授权码从授权服务器的令牌端点那里请求获取一个访问令牌。
- (E) 授权服务器对客户端进行认证,校验授权码,并确保这个重定向URI和第三步(C)中那个URI匹配。如果校验通过,则发放访问令牌,以及可选的刷新令牌。
隐式授权
隐式授权用于获取访问令牌(它不支持刷新令牌),它针对已知的操作特定重定向URI的公共客户端进行了优化。这些客户端通常在浏览器中使用脚本语言(如JavaScript)实现。
因为它是基于重定向的流程,所以客户端必须有能力和资源所有者的用户代理(典型地,是一个Web浏览器)进行交互,同时必须有能力接收来自授权服务器的重定向请求。
隐士授权类型不包含客户端身份验证,它依赖于资源所有者的存在和重定向URI的注册。由于访问令牌被编码到重定向URI中,所以它可能暴露给资源所有者以及同一台设备上的其它应用。
隐式授权流程如图所示:
- (A) 客户端引导资源所有者的user-agent到授权端点。客户端携带它的客户端标识,请求scope,本地state和一个重定向URI。
- (B) 授权服务器对资源所有者(通过user-agent)进行身份认证,并建立连接是否资源所有者允许或拒绝客户端的访问请求。
- (C) 假设资源所有者允许访问,那么授权服务器通过重定向URI将user-agent返回客户端。
- (D) user-agent遵从重定向指令
- (E) web-hosted客户端资源返回一个web页面(典型的,内嵌脚本的HTML文档),并从片段中提取访问令牌。
- (F) user-agent执行web-hosted客户端提供的脚本,提取访问令牌
- (G) user-agent将访问令牌传给客户端
资源所有者密码凭证授予
资源所有者密码凭证授予类型适用于资源所有者与客户端(如设备操作系统或高度特权应用程序)存在信任关系的情况。授权服务器在启用这种授予类型时应该特别小心,并且只在其他授权流程不可行的时候才允许使用。
这种授权类型适合于有能力维护资源所有者凭证(用户名和密码,典型地,用一个交互式的表单)的客户端。
资源所有者密码凭证流程如图:
- (A) 资源所有者提供他的用户名和密码给客户端
- (B) 客户端携带从资源所有者那里收到的凭证去授权服务器的令牌端点那里请求获取访问令牌
- (C) 授权服务器对客户端进行身份认证,并校验资源所有者的凭证,如果都校验通过,则发放访问令牌
客户端凭证
客户端用它自己的客户单凭证去请求获取访问令牌
客户端凭证授权流程如图所示:
- (A) 客户端用授权服务器的认证,并请求获取访问令牌
- (B) 授权服务器验证客户端身份,如果严重通过,则发放令牌
JWT介绍
随着技术的发展,分布式web应用的普及,通过session管理用户登录状态成本越来越高,因此慢慢发展成为token的方式做登录身份校验,然后通过token去取redis中的缓存的用户信息,随着之后jwt的出现,校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录更为简单。
使用示例
maven依赖:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
JWT工具类: 用于生成Token,和Token验证
public class JwtUtils {
/**
* 签发JWT
* @param id
* @param subject 可以是JSON数据 尽可能少
* @param ttlMillis
* @return String
*
*/
public static String createJWT(String id, String subject, long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id)
.setSubject(subject) // 主题
.setIssuer("user") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey); // 签名算法以及密匙
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate); // 过期时间
}
return builder.compact();
}
/**
* 验证JWT
* @param jwtStr
* @return
*/
public static CheckResult validateJWT(String jwtStr) {
CheckResult checkResult = new CheckResult();
Claims claims = null;
try {
claims = parseJWT(jwtStr);
checkResult.setSuccess(true);
checkResult.setClaims(claims);
} catch (ExpiredJwtException e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_EXPIRE);
checkResult.setSuccess(false);
} catch (SignatureException e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
} catch (Exception e) {
checkResult.setErrCode(SystemConstant.JWT_ERRCODE_FAIL);
checkResult.setSuccess(false);
}
return checkResult;
}
public static SecretKey generalKey() {
byte[] encodedKey = Base64.decode(SystemConstant.JWT_SECERT);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
*
* 解析JWT字符串
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}