一:spring security
1.认证
2.授权
1.xx-security 父项目
security-app 给app提供安全服务
security-browser 给浏览器提供安全
security-core 核心
父项目导入如下配置 子项目不用写版本号
<!--加入下面两个依赖,子项目不用写版本号-->
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Brussels-SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.导入核心依赖
org.springframework.cloud
spring-cloud-starter-oauth2
3.想访问RestFul服务 要做一个安全处理
Security的基本原理是一组过滤器链 有了Security 一个请求就要经过这些过滤器链
UsernamePasswordAuthenticationFilter 处理表单登陆
/
BasicAuthentication 处理HTTP Basic 登陆
/
MobileCodeAuthenticationFilter 短信验证码登陆(需要自己写)
/
RemeberMeAuthenticationFilter 读取Cookie中的token 交给RemeberMeService ,RemeberMeService会去数据库查 再调用UserDetailsService获取用户信息
/
SocialAuthenticationFilter 第三方登陆
。
。
。
Exception Translation Filter 用来处理FileSecurity Interceptor抛出的异常
/
FileSecurity Interceptor 最后一环 会决定请求究竟能不能访问Rest服务 依据代码的配置决定过不过 不过则抛出异常
Rest API
功能1:Security 默认是Basic 登陆 想换成表单登陆需要写一个配置类 XXConfig(@Configuration+@Been) extends WebSecurityConfigurerAdapter类
重写它的configure方法 去配置一些东西 比如 表单登陆.formLogin(),自定义登陆页面 .loginPage, form表单返回路径.loginProcessingUrl("/authentication/form"),自定义成功处理器AuthenticationSuccesssHandler和失败处理器ZhuAuthenticationFailureHandler 用于返回给前台一些信息
在自己写的处理器类上加上@Component注解 自自定义的这个配置类里面注入进来就可以了;让某些路径不受限 .antMatchers("/Authentication/require","/login.html","/code/imgage","/code/mobile","/authentication/mobile").permitAll()。
package com.demo.browersecurityconfig;
import com.demo.browersecurityconfig.mobile.authentication.MobileCodeAuthenticationSecurityConfig;
import com.demo.browersecurityconfig.mobile.authentication.MobileValidateCodeFilter;
import com.demo.browersecurityconfig.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 把自己写的登陆成功处理器注入进来
*/
@Autowired
private AuthenticationSuccessHandler zhuAuthenticationSuccessHandler;
/**
* 失败处理器注入进来
*/
@Autowired
private AuthenticationFailureHandler zhuAuthenticationFailureHandler;
/**
* 把数据源注入进来
*/
@Autowired
private DataSource dataSource;
/**
* 记住我功能 从数据库拿到name
*/
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private MobileCodeAuthenticationSecurityConfig mobileCodeAuthenticationSecurityConfig;
/**
* 凡是@Configuration+@Bean 注册的Bean 都可以使用@Autowired来调用
* 密码加密解密 在注册时加密 在Security时自动解密
* 加上此配置 登陆时loadUserByUsername方法中必须使用加密密码
* @return
*/
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 记住我功能
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository()
{
JdbcTokenRepositoryImpl tokenRepository =new JdbcTokenRepositoryImpl();
//启动的时候创建表 存用户名和token 再次启动请关闭
/* tokenRepository.setCreateTableOnStartup(true);*/
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//图形验证码验证过滤器
ValidateCodeFilter validateCodeFilter =new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(zhuAuthenticationFailureHandler);
//短信验证码过滤器
MobileValidateCodeFilter mobileValidateCodeFilter =new MobileValidateCodeFilter();
mobileValidateCodeFilter.setAuthenticationFailureHandler(zhuAuthenticationFailureHandler);
http .addFilterBefore(mobileValidateCodeFilter,UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
//自定义登陆页面 表单一定是 username 和password
.loginPage("/Authentication/require")
//form表单返回路径
.loginProcessingUrl("/authentication/form")
//自己写的成功处理器
.successHandler(zhuAuthenticationSuccessHandler)
//失败处理器
.failureHandler(zhuAuthenticationFailureHandler)
//记住我配置
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(2000)
.userDetailsService(userDetailsService)
//默认为Basic登陆
// http.httpBasic()
.and()
//session相关
.sessionManagement()
//session的最大数量
.maximumSessions(1)
//只能允许有一个活动的session
.maxSessionsPreventsLogin(true)
.and()
.and()
//都要验证
.authorizeRequests()
//让此路径不受限 可多个
.antMatchers("/Authentication/require","/login.html","/code/imgage","/code/mobile","/authentication/mobile","/index.html").permitAll()
//拥有指定角色时返回true
.antMatchers(HttpMethod.GET,"/user").hasRole("ADMIN")
//用户指定权限时返回true
.antMatchers(HttpMethod.GET,"/ceshi.html").hasAuthority("write")
//任何请求
.anyRequest()
//需要身份验证
.authenticated()
//关闭CSRF
.and()
.csrf().disable()
//把mobileCodeAuthenticationSecurityConfig配置加到整个配置后面
.apply(mobileCodeAuthenticationSecurityConfig);
}
}
功能2:security有一个默认的用户名和密码 如果想使用数据库里面的用户密码登陆的话 则需要写一个实现类MyUserDetailsService来实现UserDetailsService接口
并实现它的UserDetails方法 这里就可以把Service注入进来来实现数据库的操作
另外密码的加密是使用Security里面的PasswordEncoder 里面有两个方法 加密和解密 在注册的时候加密 在登陆的时候自动解密
package com.demo.browersecurityconfig;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.demo.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private IUserService iUserService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
/**
* UserDetails 里面还有几个方法
* boolean isAccountNonExpired(); 是否过期
*
* boolean isAccountNonLocked();是否锁定
*
* boolean isCredentialsNonExpired();密码是否过期
*
* boolean isEnabled();账户是否可用
*/
log.info("用户名:"+s);
//根据用户名查pwd
com.demo.entity.User user= iUserService.selectOne(new EntityWrapper<com.demo.entity.User>().eq("name",s));
//log.info("pwd:"+user.getPwd());
String pwd= passwordEncoder.encode(user.getPwd());
//log.info("加密后的密码:"+pwd);
return new User(s,pwd,true,true,true,true,AuthorityUtils.commaSeparatedStringToAuthorityList("write"));
}
}
功能3:记住我 当认证成功后把调一个RemeberMeService服务 这个服务会生成一个Token 将Token写入浏览器的Cookie 还会写入到数据库,会跟用户名一起被写入数据库
在过期时间没到的时候就不用登陆就可以访问被保护的服务了,再次登陆的时候会经过一个RemeberMeAuthenticationFilter过滤器 RemeberMeAuthenticationFilter 会读取Cookie中的token 交给RemeberMeService ,RemeberMeService会去数据库查 再调用UserDetailsService获取用户信息
功能4:图形验证码 首先写一个图形验证码的生成器用于生成图形验证码,再写一个处理验证验证码的过滤器用于判断生成的验证码和输入的验证码是否匹配
在生成验证码的时候把验证码存到session里面 验证的时候就是把输入的验证码和sesion里面的验证码做一个对比
如果正确则调用下一个过滤器 不正确则进入失败处理器抛出相应的异常。
还要在配置里面配置.addFilterBefore 把自己写的图形验证码的过滤器 放在UsernamePasswordAuthenticationFilter过滤器的前面
功能5:短信登陆
用户名和密码登陆的一个执行流程是
UsernamePasswordAuthenticationFilter—>拿用户名和密码组装成一个UsernamePasswordAuthenticationToken(未认证)对象传给 AuthenticationManager
AuthenticationManager会从一堆DaoAuthenticationProvider去挑一个Provider来处理这个认证请求,挑的依据是DaoAuthenticationProvider里面有一个 supports方法来判断你传进来的这个token 支持的话 就会认证这个token 在认证的过程中会调用UserDetailsService来获取用户信息 跟传进来的信息做一些比对
认证通过会把这个token做一个标记 标记为已认证 放在session中
那么,短信登陆是两种不同的登陆方式 不能用这套登陆方式
所以是仿照这套流程写一套自己的认证流程 要写4个东西
1.MobileCodeAuthenticationFilter 来拦截短信登陆请求--->拿到一个手机号-->封装成自己写的一个MobileAuthenticationToken--这个token也会传给 AuthenticationManager 它依然会检索系统中所有的DaoAuthenticationProvider-->这时就要自己写一个短信的MobileCodeAuthenticaionProvider 用它来校验
手机号的信息 校验的过程中依然会调用 UserDetailsService把手机号传给它 让他去读用户信息 去判断是否能登陆 如果通过 把自己写的短信的tokon标记为 已认证-->最后还要写一个过滤器来验短信验证码 也是要放在MobileCodeAuthenticationFilter之前 这几个东西写完之后还要写一个配置类把写的这几个东西组 装一下写到security中去 最后把这个配置类注入到XXConfig中去 用.apply把这个配置类放在整个配置的后面,还有短信验证码验证的这个过滤器 要.addFilterBefore放到UsernamePasswordAuthenticationFilter的前面
功能5:第三方登陆 QQ ,微信
是利用spring Social开发第三方登陆的 spring Social是基于OAuth协议的,OAuth协议就是一个授权协议,目的是让用户不将服务提供商的用户名密码交给第三方应用的情况下,让第三方应用可以有权限访问用户存在服务提供商上面的一些资源 这是OAuth协议存在的意义。
OAuth协议的一个流程是 用户–访问–第三方应用–第三方应用将用户导向服务提供商(Provider)的认证服务器(Authorization Server)–用户同意授权后返回第三方应用并携带授权码–第三方应用携带授权码向服务提供商(Provider)的认证服务器(Authorization Server)申请令牌–服务提供商(Provider)的认证服务器(Authorization Server)向第三方应用发放令牌–这样第三方应用就可以获取用户存在服务提供商的一些资源
第三方登陆:引导用户走完这个流程就相当于用户使用在服务提供商上面的用户信息登陆了我们的第三方应用 即第三方登陆
spring Social干了什么?spring Social把整个流程封装到了一个SocialAuthenticationFilter过滤器里面,然后加入到了spring security过滤器链上,当有一个请求过来 这个过滤器就会拦截 带着你把整个流程走完 其实就是实现了微信登陆。
开发过程中涉及到的具体的类是
要实现一个第三方登陆就要拿到服务提供商的用户信息
服务提供商的用户信息是封装到一个Connection类中–要想构建一个Connection就需要一个ConnectionFactory工厂–构建ConnectionFactory工厂需要两个东西:1.服务提供商(ServiceProvider)和2.ApiAdapter(Api适配器)–要构建ServiceProvider也要两个东西:1.OAuth2Operations(spring social提供了一个默认实现OAuth2Temlate) 2.Api读取用户信息的 。要想知道在服务提供商的A用户登陆其实是业务系统中的张三登陆 ,这一个对应关系是存在数据库中的(UserConnection表)
是谁来操作这张表呢?是UserConnectionRepository (只需要配置一下数据库的位置即可) 针对这张表做增删该查的操作
—登陆成功之后用户的信息都是存在session里的 就会有一下问题
功能6:sesion超时问题 在yml里面配置 :
server:
port: 8084
#session失效时间 默认30min 单位是秒
session:
timeout: 10
session最少是1分钟 session失效要返回一个session失效的信息给前台 在security里面配置session失效跳转路径
.sessionManagement()
.invalidSessionUrl(“xxx”)
功能7:session并发 控制一个session只有一个用户 后面登陆的不能登陆
在配置里面配置允许存在的sesion的数量 设成1 还要把 .maxSessionsPreventsLogin(true)
//session的最大数量
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
功能8: 集群环境下sesion问题 比如说在集群环境下 至少要部署两台机器 ,如果用户的登陆请求是发到A机器 那么登陆成功的认证信息是放到A服务器的session里面的
那么后续的请求也许会发到B服务器上 那么B服务器里面的session并没有认证信息 那么B服务器会拒绝这个请求,会要求用户再次登陆
解决办法 :就是别把session信息放到服务器上 把它抽出来 存在一个独立的存储里面 这样就不会出现那个问题了 spring提供了一个项目来处理这个问题
就是依赖 spring.session 只需要指定这个项目指定的存贮是什么这 支持很多类型 这里用的是Rredis 为什么用Resis 1.因为session是一个频繁访问的东西 spring security是在请求之前加一个过滤器链 实际上每一个请求都会过这些过滤器链 每次都会去读session里面的内容 如果放在数据库里 这个读取的压力就会特别大 2.session本身是有一个过期时间的 如果存到数据库中那么还需要去手动维护数据库 Redis本身就带有失效性这样一个特征 把数据设进去的时候 就可以设置一个超时时间 到了这个时间 Redis就会把失效的时间自动清了
功能9: spring security OAuth 开发App认证框架
之前讲的这些都是基于浏览器访问服务器这样这样方式 浏览器发一个请求 服务器都会去检查浏览器的Cookie里是否存在JSESSIONID 如果不存在JSESSIONID
就会去服务器上新建一个session 然后把新建的这个session的id写到浏览器的Cookie的jssionid里 这样每次请求的时候浏览器就会拿着jssionid去服务器找 ssion 这样就会拿到信息。
APP出现了 前后端技术出现了 html的页面不再和应用服务器一起部署了 而是单独部署到一个webserver上 这样用户访问是就是webserver服务,由webserver服务再去访问应用服务器 用户不再直接访问应用服务器了 而是去访问一个第三方应用。
当访问的是一些第三方应用的时候就需要使用一种新的方式来存贮登陆信息 就是Token(令牌)的方式 应用服务器直接给用户一个令牌,用户每次访问的时候就要带着这个令牌来访问 应用服务器是通过这个令牌来判断这是用户是谁 它有什么权限的
spring security OAuth已经封装了服务提供商的大部分内容 服务提供商(这里相当于就是应用服务器) App相当于就是第三方应用 spring security OAuth
里面有一个过滤器,也是放在过滤器链上的 作用是从请求中你拿出来你发出去的token 然后根据配的存贮策略 去存贮里找到这个token对应的用户信息 然后根据这个用户信息来判断是否有权限访问我们的服务。
功能10:授权: 分两种
1.针对普通用户来使用的 只区分是否登陆,只区分简单角色,权限规则基本不变的 这中情况可以写代码在代码里面配置,比如访问哪一个URL需要一个什么角色
在登陆的时候给他什么角色或者什么权限就可以了 如果该用户没有该角色 是不能访问的
例: //拥有指定角色时返回true
.antMatchers(HttpMethod.GET,"/user").hasRole("ADMIN")
//用户指定权限时返回true
.antMatchers(HttpMethod.GET,"/demo.html").hasAuthority("write")
常见的权限表达式: permitAll 永远返回true 不需要任何角色和权限
rememberMe 当用户是rememberMe用户的时候返回true
hasRole(role) 当用户拥有指定角色的时候返回true 注意在登陆的时候大小写一定要一直 还要必须要有ROLE_这个前缀
hasAuthority(authority) 当用户拥有指定权限的时候返回ture
hasAnyAuthority([authority1,authority2]) 当用户拥有任意一个指定权限的时候返回ture
2.内管系统 给公司的运营人员使用 角色众多权限复杂 权限规则随着公司的发展不断变化 这个时候就需要自己开发一个权限模块 把这些权限和规则存到数据库中去 这里就有5张表:
RBAC数据模型 三张实体表 两张关系表
都是多对多的关系
用户表(存贮用户信息 由业务人员维护)
角色表
资源表 (菜单 按钮 已经对应的url)
用户--角色关系表
角色-资源关系表
这样就可以通过用户名来查找相应的角色和相应的url也就是权限