目录
一、简介
二、使用
引入
登录验证流程
完整流程
三、案例(登录验证)
三、设置密码加密和解密方式
三、自定义登录
四、定义JWT认证过滤器
简介
流程
JWT(当前未使用)
定义token过滤
配置过滤器为最前
再次总结流程
五、退出登录
六、一些中间配置
七、网关的配置
八、授权
权限控制(开启)
赋予用户权限
从数据库查询权限信息
权限判断注解
自定义权限校验
九、自定义失败处理
十、处理跨域请求
对Springboot配置跨域
Spring Security配置允许跨域
十一、 CSRF
十二、总结
一、简介
安全框架,做web的认证和授权
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限
二、使用
引入
<!-- 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
登录验证流程
前端登录(携带用户名密密码访问登录接口)------服务端(去和数据库中的用户名和密码进行比较检验,如果正确就生成一个jwt)----jwt传输给前端
登录后访问其他请求,需要携带请求头中携带token----服务器根据token获取相关数据---响应信息
完整流程
原理是一个过滤器链
请求----> UsernamePassword AuthenticationFilter-->ExceptionTranslationFilter-->FilterSecurityLnterceptor-->api
响应 <------------------------------<--------------------------------<---------------------------------
等等过滤器
UsernamePassword AuthenticationFilter :登录页面填写了用户名
ExceptionTranslationFilter:处理抛出的异常
FilterSecurityLnterceptor:负责权限校验的过滤器
三、案例(登录验证)
登录
1、自定义登录接口
调用ProviderManager 的方法进行认证,如果通过生成jwt
把用户信息存入redis中
2、自定义UserdetilsService这个实现列中去查询数据库
在这个实现列中去查询数据库
校验:
1、定义jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContexHolder
依赖文件
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- fastjson依赖-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.26</version>
</dependency>
<!-- jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
login登录验证
UserDetailsServiceImpl 实现校验,校验的主题
//自定义的登录验证链
//实现UserDetailsService方法即可,返回一个用户对象
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private MyUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<MyUser> queryWrapper=new LambdaQueryWrapper<>();
// 等值匹配
queryWrapper.eq(MyUser::getName,username);
MyUser user2=userMapper.selectOne(queryWrapper);
//如果没有查询结果 抛出异常
if(Objects.isNull(user2)){
throw new RuntimeException("用户不存在或者密码错误");
}
//TODO 查询对应的权限信息
// 把数据封装成LoginUser(UserDetails)
return new LoginUser(user2);
}
}
数据库中需要 {noop}1234 来表明是明文
UserDetails
把user封装成校验信息的User--即校验的数据(LoginUser)
//用户数据对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private MyUser user2;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 返回权限信息
return null;
}
@Override
public String getPassword() {
return user2.getPwd().toString(); //获取用户密码
}
@Override
public String getUsername() {
return user2.getName();
}
@Override
public boolean isAccountNonExpired() {
return true; // 是否每过期
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true; //是否没有超时
}
@Override
public boolean isEnabled() {
return true; // 用户是否可用
}
}
三、设置密码加密和解密方式
数据库中密码是加密的,网关它会自动解密对比
配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//创建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder(); // 加密以及密码验证方式
}
}
测试使用加密解密
@Autowired
PasswordEncoder passwordEncoder;
@Testpublic void test01(){
BCryptPasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
//加密
String encode1 = passwordEncoder.encode("1234");//加密
log.info("加密一次:{}",encode1);
// 密码对比 明文--密文
boolean matches = passwordEncoder.matches("1234", "$2a$10$asn3beY//msSOAJjnlmY/ei1ZqKi1q4//ivsLPA44mSMFEa6X1XOW");
log.info("登录成功:{}",matches);
}
三、自定义登录
前提:放行登录接口
流程:
- 登录--调用验证方法--登录成功生成jwt--token存入redis
-
调用验证方法:创建验证链--包装user和pwd--调用验证方法
配置类放行接口
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//设置拦截放行
@Override
protected void configure(HttpSecurity http) throws Exception {
http //关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext--认证之后的用户信息
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//接口放行设置
.authorizeRequests()
//对于登录接口 允许匿名访问 (SecurityContext没有值才能通过网关)
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要网关权限认证
.anyRequest().authenticated();
}
//注入验证链
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//密码加密和解析
//创建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
接口调用方法
//自定义的登录验证
@Service
public class LoginServiceImpl implements LoginService {
@Autowired //验证链
private AuthenticationManager authenticationManager;
@Autowired //redfis
StringRedisTemplate redisTemplate;
@Override
public String login(MyUser user) throws RemoteException {
log.info("数据:{}",user);
//1、 把账号和密码进行包装--根据账号密码生成验证对象
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user.getName(),user.getPwd());
//验证方式是UserDetails中的验证方法,对封装的对象进行验证,如果验证成功会返回一个Authenticate对象
Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 把对象传入验证链中,这里验证链是自定义的
//2、 如果没通过,给出对应的提示,认证没同过就是空
if(Objects.isNull(authenticate)){
throw new RemoteException("登录失败");
}
//3、 如果认证通过了,使用userid生成一个jwt
//获取验证成功的对象 获取验证的成功的用户
LoginUser loginUser=(LoginUser) authenticate.getPrincipal();
String name = loginUser.getUser2().getName();
//TODO 生成jwt
//4、 TODO 把完整的用户信息存入redis ,userName作为key
redisTemplate.opsForValue().set(user.getName(), "token");
return "登录成功";
}
}
自定义登录验证链一环(上文解决问题实现)
//自定义的登录验证链
//实现UserDetailsService方法即可,返回一个用户对象
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private MyUserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
LambdaQueryWrapper<MyUser> queryWrapper=new LambdaQueryWrapper<>();
// 等值匹配
queryWrapper.eq(MyUser::getName,username);
MyUser user2=userMapper.selectOne(queryWrapper);
//如果没有查询结果 抛出异常
if(Objects.isNull(user2)){
throw new RuntimeException("用户不存在或者密码错误");
}
//TODO 查询对应的权限信息
// 把数据封装成LoginUser(UserDetails)
return new LoginUser(user2);
}
}
四、定义JWT认证过滤器
简介
过滤拦截全部请求,配置在网关签名,当前jwt过滤器放行后才会进入网关验证
网关会读取存储在SecurityContexHolder 用户密码进行认证;
流程
总结为:
定义jwt过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContexHolder
放行--进入网关验证
JWT(当前未使用)
是一种格式字符串(加密串),它允许设置过期时间
组成:
由三部分组成
Header 头部--类型--固定
Playload 负载--明文信息--自定义
Signature 再加密
由 . 拼接
依赖:
<!-- jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
生成jwt
@Test
public void Test(){
// 创建一个jwt对象
String jwtToken=Jwts.builder()
// 1、Header头部--类型--固定
.setHeaderParam("typ","JWT") //类型 固定 是typ
.setHeaderParam("alg","HS256") //算法 固定
// 2、 Payloda: 负载--自定义信息--明文
.claim("username","1234") //自定义
.setSubject("admin-test") //自定义
.setExpiration(new Date(System.currentTimeMillis()+1000*50))// Toekn过期时间 ms
// id字段 可以存储 账号+uuid
.setId(UUID.randomUUID().toString())
// 3、签名
.signWith(SignatureAlgorithm.HS256,"admin") //设置加密算法和--签名, 用于解密操作
.compact(); //使用.生成完整的字符串
log.info("JWT:{}",jwtToken);
}
解析jwt
// 解析jwt
@Test
public void Test03(){
// jwt串
String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IjEyMzQiLCJzdWIiOiJhZG1pbi10ZXN0IiwiZXhwIjoxNjg4OTEwNDMyLCJqdGkiOiJlMjVkY2IwNy02MTc2LTQyY2ItYWYxOC02YWM3Mzg4YmM4MDMifQ.t4AqR4ni9UxCWuDXx07WQDJIv2CDadGXWR2pGOGi7Tk";
try {
Jws<Claims> jwt= Jwts.parser() //解析
.setSigningKey("admin") //使用令牌 ,令牌要对应
//类似 Map集合
// 将jwt转化成 key-value结构,如果过期了,那么将解密失败
// 获取jws对象中的数据
.parseClaimsJws(token);
log.info("数据:{}",jwt.getBody().getId());
log.info("数据:{}",jwt);
}catch (Exception e){
log.error("jwt失效");
}
}
定义token过滤
//过滤器
@Component
public class JwtAuthenticationTokenFiler extends OncePerRequestFilter {
@Autowired
StringRedisTemplate redisTemplate;
//不拦截的请求---响应时候也会执行,但不会被干扰doFilterInternal
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
StringBuffer requestURL = request.getRequestURL(); //获取url
String requestURI = request.getRequestURI(); //获取uri
log.info("放行器当前请求:{}",requestURI); //但还是要先经过网关
if("/user/qt".equals(requestURI)){
log.info("放行请求,{}",requestURL);
//不拦截 "/user/qt" 请求
return true;
}
return false; //false 就会执行doFilterInternal
}
//拦截的请求
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1、-- 获取token--
String token=request.getHeader("token");
if(!StringUtils.hasText(token)){
//没有token就放行--放行到网关,没有账号密码网关不会被通过,所以当前请求无效
SecurityContextHolder.getContext().setAuthentication(null); //无token就清空 SecurityContextHolder域
filterChain.doFilter(request,response); //authenticationToken 为空将不会,放行后也不会执行后面的过滤链操作
return; //如果不return;后面响应返回来 还会继续执行
}
//TODO jwt解析token 获得获得账号
// 2、-- redis根据账号查询得到用户对象
String user = redisTemplate.opsForValue().get(token);
LoginUser loginUser= JSON.parseObject(user, LoginUser.class);
if(Objects.isNull(loginUser)){
//应该不会执行这个--除非token非法
throw new RuntimeException("用户未登录");
}
//3、--存入SecurityContextHolder--后面的操作都是从这里面获得信息的
// TODO 把数据封装到Authentication中,最后一个参数为为权限
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(loginUser.getUsername(),loginUser.getPassword(),null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken); //存放数据,//如果authenticationToken为null时候将不会执行后面的过滤连
//4、-- SecurityContextHolder携带账号信息放行到网关
filterChain.doFilter(request,response); //放行到下一个过滤器,无下一个过滤器所以直接到网关 }
}
配置过滤器为最前
配置在网关过滤前面
@Configurationpublic
class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired //注入token过滤器
private JwtAuthenticationTokenFiler jwtAuthenticationTokenFiler;
//设置拦截放行
@Override
protected void configure(HttpSecurity http) throws Exception {
http //关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext--认证之后的用户信息
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//接口放行设置
.authorizeRequests()
//网关放行 登录接口
.antMatchers("/user/login").anonymous() //网关放行请求
// 除上面外的所有请求全部需要权限认证
.anyRequest().authenticated(); //其他的请求网关都拦截
// token过滤拦截配置在 网关拦截前面
http.addFilterBefore(jwtAuthenticationTokenFiler, UsernamePasswordAuthenticationFilter.class);
}
//注入验证链
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//密码加密和解析
//创建BCryptPasswordEncoder注入容器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
再次总结流程
请求-->token拦截(需要正确token才能通过)-->网关拦截(需要账号和密码才能通过)-->(过滤器逆顺序执行一遍)-->服务
token拦截:需要配置shouldNotFilter方法配置不拦截的请求,拦截的请求放行后到达网关(SecurityContextHolder携带账号密码);
网关拦截:在网关配置链中配置不拦截的请求,拦截的请求需要账号密码才会被放行。(默认会跳转一个登录页面);
token拦截配置在网关拦截前面
登录请求--配置token放行--配置网关网关放行--自定义的登录验证(框架中提供了一个UserDetailsService验证接口,用AuthenticationManager执行验证链)---服务
五、退出登录
在token过滤器身份校验时候把信息从redis读取到了SecurityContexHolder中,只需要删除redis中存储的数据即可,
因为验证时候是先去redis中查找数据再存储到SecurityContexHolder中,可以根据SecurityContexHolder中的数据找到redis中的数据
实现服务
退出一般为登录状态,即拥有token
public void loginOut() throws RemoteException {
//获得SecurityContexHolder中的用户id
UsernamePasswordAuthenticationToken user =(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
// 或者
// Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.info( "authentication中的user数据:{}",authentication.getPrincipal());
//删除redis中的值
if(!Objects.isNull(user)){
//不为空就删除数据
redisTemplate.opsForValue().getAndDelete((String) user.getPrincipal());
log.info("{},退出成功:",user.getPrincipal());
return;
}
//没有数据
throw new RemoteException("用户未登录");
}
六、一些中间配置
UserDetails 接口,继承它的类为验证数据User;
UsernamePasswordAuthenticationToken 类型中间变量,封装账号和密码
principal:账号
credentials:密码
Authentication:类型: 验证链验证的放回的结果
principal: 实现UserDetails的User类,SecurityContextHolder.getContext()返回的Authentication 是账号
credentials:密码
Authorities :权限集合
AuthenticationManager 类型: 验证链
SecurityContextHolder.getContext():数据存放的位置,存放UsernamePasswordAuthenticationToken
七、网关的配置
链式配置
配置项目
authorizeRequests()
csrf()
formLogin()
httpBasic()
antMatcher()
等等
返回链式配置可以调用放回值为HttpSecurity的属性
禁用csrf保护
http.csrf().disable()
请求拦截:
authorizeRequests()
http.authorizeRequests().antMatchers("请求").anonymous() //SecurityContextHolder.getContext()中无数据才放行
http.authorizeRequests().antMatchers("请求").permitAll(); //直接放行当前请求,无论SecurityContextHolder.getContext() 是否有数据
.anyRequest().authenticated(); //(其他请求)认证后才可以访问
添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFiler, UsernamePasswordAuthenticationFilter.class); //token验证在网关先执行
八、授权
权限控制(开启)
不同的用户使用不同的功能,必须具有所需权限才能进行相应的操作
配置类加入注解开启权限控制
@Configuration //配置类
//在配置类上加上当前注解,来表示开启当前权限认证功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
权限
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('test')") //是否有当前权限,必须有test权限才能访问
public String hello(){
return "hello";
}
赋予用户权限
UserDetails添加权限字段属性
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
//账号密码
private MyUser user2;
//权限
private List<String> permissions;
@JSONField(serialize = false) //方法不进行序列化
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { //放回权限信息
// 返回权限信息,封装成,SimpleGrantedAuthority对象,GrantedAuthority的实现类
List<SimpleGrantedAuthority> list =new ArrayList<>();
permissions.forEach(i->{
SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(i);
list.add(simpleGrantedAuthority);
});
return list;
}
@JSONField(serialize = false)//方法不进行序列化
@Override
public String getPassword() {
return user2.getPwd().toString(); //获取用户密码 }
@JSONField(serialize = false)//方法不进行序列化
@Override
public String getUsername() {
return user2.getName(); }
@JSONField(serialize = false)//方法不进行序列化
@Override
public boolean isAccountNonExpired() {
return true; // 是否每过期 }
@JSONField(serialize = false)//方法不进行序列化
@Override
public boolean isAccountNonLocked() {
return true; }
@JSONField(serialize = false)//方法不进行序列化
@Override
public boolean isCredentialsNonExpired() {
return true; //是否没有超时 }
@JSONField(serialize = false)//方法不进行序列化
@Override
public boolean isEnabled() {
return true; // 用户是否可用 }
}
从数据库查询权限信息
从数据库查询权限信息进行封装
RBAC权限模型:
基于角色的权限控制,这是目前最常被开发者使用也是相对易用、通用权限模型。
例如:
例如:
用户表(小王,小红)
角色表 (管理员,读者)
关联表(小王-管理员,小王-读者,小红-读者)
用连接查表,三个表多表联查,查出权限
<select id="com.example.demo2.mapper.MeunMapper.getUserByUser" resultType="string">
SELECT DISTINCT role_key
FROM user_role
left JOIN my_user ON user_role.myuser_id=my_user.id
LEFT JOIN role ON user_role.role_id=role.id
WHERE name=#{user};
</select>
权限判断注解
hasAnyAuthority 方法可以传入多个权限,只要用户拥有任意一个权限就可访问
@PreAuthorize("hasAnyAuthority('a','b','c')") //拥有 a、b、c任意一个即可访问
自定义权限校验
使用权限校验
自定义方法
@Component("ex")//重命名
/** * 自定义权限校验 */
public class expression {
public boolean hasAuthority(String authority){
// 获取当前用户的权限 ,从域中获得用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//存储 权限集合
List<String>list=new ArrayList<>();
// 获得权限集合
authentication.getAuthorities().forEach(i->{
list.add(i.toString());
});
// 如果拥有对应权限
if(list.contains(authority)){
return true;
}
//当前权限不在权限集合中
return false;
}
}
使用自定义权限校验
@PreAuthorize("@ex.hasAuthority('test')") //从Bean中找到ex类对象的hasAuthority方法
@RequestMapping(value = "/user/qt",method = RequestMethod.POST)
public String login() throws RemoteException {
// 登录
log.info("qt执行成功");
return "qt";
}
九、自定义失败处理
我们还希望在认证失败或者授权失败的情况下也能和我们的接口一样返回相同的json,这样可以让前端能对响应进行统一的处理;
认证失败:
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntrypoint
对象方法去进行异常处理;
授权失败:
如果是授权过程中出现异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法
去进行异常处理;
如果定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可
处理登录异常:
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
//处理认证异常
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//处理异常
log.info("当前请求:{} 认证失败",request.getRequestURL());
response.setContentType("application/json");//返回类型
response.setStatus(401);
//返回相应体
response.getWriter().println(JSON.toJSONString("用户认证失败,请重新登录")); //返回消息
}
}
权限不够异常:
/** * 处理权限不够的异常 */
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//处理异常
log.info("当前请求:{} 权限不够",request.getRequestURL());
response.setContentType("application/json");//返回类型
response.setStatus(403);
//返回相应体
response.getWriter().println(JSON.toJSONString("您的权限不够"));
//返回消息
}
}
设置配置类中
// 配置异常处理器
http.exceptionHandling()
//配置认证失败处理
.authenticationEntryPoint(authenticationEntryPoint)
//配置权限不够处理
.accessDeniedHandler(accessDeniedHandler);
十、处理跨域请求
对Springboot配置跨域
配置类全局配置
@Configuration
//springboot设置跨域
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域的请求域名
.allowedOriginPatterns("*")
//是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET","POST","DELETE","PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许的时间 --不再询问
.maxAge(3600);
}
}
@CrossOrigin注解局部方法配置(更加方便)
@CrossOrigin(origins = "http://localhost:8080") //允许当前访问
@RequestMapping(value = "/user/qt",method = RequestMethod.POST)
public String login() throws RemoteException {
// 登录 log.info("qt执行成功");
return "qt";
}
Spring Security配置允许跨域
配置类中设置如下:
http.cors(); //直接这个就表示允许跨域
十一、 CSRF
CSRF是指跨站请求伪造,是web常见的攻击之一
前后端分离需要关闭,不然会自动校验
http //关闭csrf
.csrf().disable()
十二、总结
登录流程
登录请求--token无数据放行--网关直接放行--登录接口--调用登录服务(登录部分使用验证链)
其他请求
登录请求--token有数据放行--网关根据账号密码放行--服务
配置
验证链:AuthenticationManager
加密方式:PasswordEncoder
过滤器:token过滤配置在网关验证前