Spring Security到底是什么

Spring Security你懂了吗

前置知识

  • 掌握Spring框架
  • 掌握SpringBoot框架
  • 掌握JavaWeb框架

下面的内容主要是用来介绍用户认证和用户的授权

两个安全认证框架对比

SpringSecurity

  • 能够和Spring进行无缝整合
  • 全面的权限控制
  • 专门为Web开发而设计的
    • 旧版本不能脱离 Web 环境使用
    • 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独
      引入核心模块就可以脱离 Web 环境

Shiro

是一款轻量级的权限控制框架

  • 轻量级。 Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求
    的互联网应用有更好表现
  • 通用性
    • 好处 不局限于 Web 环境,可以脱离 Web 环境使用。
    • 坏处 在 Web 环境下一些特定的需求需要手动编写代码定制

用户认证

简单的说指定就是系统认为用户是否能登录

用户授权

指定就是系统判断用户是否具有权限去做某些事情

SpringSecurity小测试

配置环境

   		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

编写Controller

@RestController
@RequestMapping("/test")
public class TestController {
    @GetMapping("/hello")
    public String add(){
        return "Hello Spring Security";
    }
}

此时启动服务进行登录之后 会出现安全认证界面

在这里插入图片描述

注意 这里的用户名默认是user 密码会在控制台打印出来

SpringSecurity基本原理

SpringSecurity本质是过滤器链 本文主要讲解三个过滤器

  • FilterSecurityInterceptor是一个方法级的权限过滤器, 基本位于过滤链的最底部

在这里插入图片描述
具体invoke方法的实现
super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务
在这里插入图片描述

  • ExceptionTranslationFilter: 是一个异常过滤器,用来处理在认证授权的过程总抛出的异常
  • 在这里插入图片描述
  • UsernamePasswordAuthenticationFilte 实现对/login的POST请求的拦截,检验表单中用户名与密码

用户自定义开发的时候不能用户名与密码都是涉及数据库的 所以在自定义开发中设计到的两个重要地接口
在这里插入图片描述

  • UserDetailsService 用来查询数据库的用户名和密码的过程

创建类继承UsernamePasswordAuthenticationFilte ,重写attemptAuthentication successfulAuthentication unsuccessfulAuthentication三个方法

创建类实现UserDetailsService 编写查询数据库的过程,返回User对象 这个User对象是安全框架提供的对象

  • PasswordEncoder

对密码进行解密

Web权限方案

  • 认证

    • 第一种 通过配置类来实现

      spring.security.user.name=123
      spring.security.user.password=123
      
    • 第二种 通过配置类来实现

      @Configuration
      public class SecurityConfig extends WebSecurityConfigurerAdapter {
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
              String password = passwordEncoder.encode("123");
              //可以通过auth设置登录名与密码
         auth.inMemoryAuthentication().withUser("123").password(password).roles("admin");
          }
          @Bean
          PasswordEncoder password(){
              return new BCryptPasswordEncoder();
          }
      }
      
    • 第三种 自定义编写实现类

      • 创建配置类 设置使用哪个userDetailService实现类

      • 编写实现类 返回User对象 User对象有用户名 买吗和操作权限

        /**
         * SpringSecurity的配置类
         */
        @Configuration
        public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
            @Autowired
            private UserDetailsService userDetailsService;
            @Override
            protected void configure(AuthenticationManagerBuilder auth) throws Exception {
                //在设置的userDetail里面返回用户名 密码 权限
                auth.userDetailsService(userDetailsService).passwordEncoder(password());
            }
            @Bean
            PasswordEncoder password(){
                return new BCryptPasswordEncoder();
            }
        }
        @Service("userDetailsService")
        public class UserDetailService implements UserDetailsService {
            @Override
            public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
                //如果涉及到数据库  根据name 查询数据库对应的数据
                List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
                return new User("ljx",new BCryptPasswordEncoder().encode("123"),auths);
            }
        }
        

连接数据库 完成用户的认证

添加pom.xml文件

		<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</ groupId>
            <artifactId>mysql-connector-java</ artifactId>
        </ dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

设置实体类 (包含username和password)

创建UserMapper继承BaseMapper

在UserDetailService里面根据传入的用户名获取对应的数据库对象 进行判断

@Service("userDetailsService")
public class UserDetailService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 这里面接收的username 就是security界面传递的用户名
        QueryWrapper<Users> qw = new QueryWrapper<>();
        qw.eq("username",username);
        Users users = userMapper.selectOne(qw);
        if (users==null){
            //认证失败
            throw new UsernameNotFoundException("对不起 用户名不存在");
        }

        //如果涉及到数据库  根据name 查询数据库对应的数据
        List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
        return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths);
    }
}

自定义登录页面以及设置哪些访问不需要设置就能实现访问

在配置类里面设置过滤规则

/**
 * SpringSecurity的配置类
 */
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //在设置的userDetail里面返回用户名 密码 权限
        auth.userDetailsService(userDetailsService).passwordEncoder(password());
    }
    @Bean
    PasswordEncoder password(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login.html") //登录页面设置
                .loginProcessingUrl("/user/login")//自定义的登录访问路径要与表单提交到地址一致
                .defaultSuccessUrl("/test/index").permitAll()//登录成功之后跳转的路径
                .and().authorizeRequests()
                .antMatchers("/","/test/hello","/user/login").permitAll()
                 //设置那些路径不需要进行访问
                .anyRequest().authenticated()
                .and().csrf().disable();//关闭csrf防护
    }
}

设置默认的登录界面 路径resourcess/statis/login.html

启动测试:

localhost:8111/test/hello==>得到输出结果

localhost:8111/test/index==>就会跳转到登录界面 然后进行登录 就能够得到对应的返回值

基于角色或权限进行访问控制

hasAuthority

如果当前的用户具有指定的权限那么返回true 否则返回false

  • 在配置类里面设置哪些路径需要指定权限

  • 在UserDetailsService里面 把返回User对象设置权限

  • 在配置类中
    .antMatchers("/test/index").hasAuthority("admins")//设置当前的登录用户  只有具有了admin权限才能够访问这个路径
    
  • 在UserDetailsService中
    List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
    

hasAnyAuthority

如果当前的主体有任何任何的角色的话返回true

hasAnyAuthority("admins,admin1")

hasRole

如果用户具备给定的角色就允许访问否则出现403

.antMatchers("/test/index").hasRole("sale")
    底层会在sale前面加ROLE_sale
 所以 
 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");

hasAnyRole

如果允许具备的多个角色就允许访问 否则403

.antMatchers("/test/index").hasAnyRole("sale,sale1")
   
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale,ROLE_sale1");
注意:在设置角色的底层会默认为其加上ROLE_ 所以在为用户赋予角色的时候也要加上ROLE_

自定义403没有权限的界面

在配置类里面
http.exceptionHandling().accessDeniedPage("/norole.html");

认证授权中注解的使用

使用注解要先开启注解的功能

@ Secured

先开启注解功能@EnableGlobalMethodSecurity(securedEnabled=true)

判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“

在控制器上也可以在方法上 针对于某一个方法

@RestController
@RequestMapping("/test")
//@Secured({"ROLE_admin123"})

只有在UserDetails里面为用户设置了权限才能够进行访问
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin123");

@PreAuthrorize

开启注解@EnableGlobalMethodSecurity(prePostEnabled = true)

作用在方法上

注解适合进入方法前的权限验证,可以将登录用户的 roles/permissions 参数传到方法中

在方法上设置
@PreAuthorize("hasAnyAuthority('admin')")
在UserDetails设置
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

@PostAuthorize

开启注解@EnableGlobalMethodSecurity(prePostEnabled = true)

在方法执行之后进行执行 基本不用 方法之后检验还有啥作用

@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
在方法上设置
@PostAuthorize("hasAnyAuthority('admin')")
在UserDetails设置
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");

@PostFilter

权限认证之后,对返回的数据进行过滤

@GetMapping("getAll")
@PreAuthorize("hasRole('ROLE_ 管理员')")
@PostFilter("filterObject.username == 'admin1'")
public List<UserInfo> getAllUser(){
ArrayList<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1l,"admin1","6666"));
list.add(new UserInfo(2l,"admin2","888"));
return list;
}
只返回username为admin1的数据

@PreFilter

对进入控制器之前的数据进行过滤

@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_ 管理员')")
@PreFilter(value = "filterObject.id%2==0")
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo>list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}
只有list集合里面Id的值是偶数的才会进入

登录之后用户的注销或者退出操作

在配置类里面添加配置退出的映射

http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
此时修改一下 登录成功的跳转界面 
.defaultSuccessUrl("/success.html").permitAll()//登录成功之后跳转的路径
在success.html里面
<a href="/logout">退出</a>

CSRF

跨站请求伪造,默认情况下回启动CSRF保护,以防止CSRF攻击应用,SpringSecurity会针对PATCH POST PUT DELETE方法进行防护
在这里插入图片描述

SpringSecurity微服务权限方案

  • 基于Session 那么Spring-Security会对cookie里面的sessionId进行解析,找到服务器存储的session信息,然后判断当前的用户是否符合请求的要求
  • 如果是token 那么就要解析出token然后将当前请求加入到Spring-Security管理的权限信息中

在这里插入图片描述

用户登录成功之后 查询对应的用户权限列表—>将用户相关信息保存在Redis里面(Key:用户名 value:用户的权限列表)—>根据用户名生成token(使用JWT)—>将token放到cookie里面 在header放token---->Spring Security 从header中获取token 那token获取用户名 那这个用户名查询对应的权限列表(Redis里面)

微服务权限管理案例的主要功能

  1. 登录(人证 )
  2. 添加角色
  3. 为角色分配菜单
  4. 添加用户
  5. 为用户分配角色

权限管理数据模型

菜单表 角色菜单表 角色表 用户角色表 用户表
在这里插入图片描述
案例涉及到的技术

Maven SpringBoot MybatisPlus SpringCloud(GetWay Nacos) Redis Swagger

创建一个父工程: acl_parent 管理依赖的版本

在父工程下面创建子模块

  • common
    • service_base: 工具类
    • spring_security: 权限配置
  • infrastructure
    • api_getway: 网关
  • service
    • service_acl 权限管理模块

在这里插入图片描述
启动Redis和Nacos服务

Redis相当于一个数据库 用来存储数据

Nacos 就是一个注册中心

在这里插入图片描述
项目操作步骤

  • 编写service_base里面的工具类的内容
  • 编写spring_security认证授权的工具类

DefaultPasswordEncoder 密码处理

//密码处理工具类
public class DefaultPasswordEncoder implements PasswordEncoder {
    public DefaultPasswordEncoder(){
        this(-1);
    }
    public DefaultPasswordEncoder (int strength){

    }
    //实现加密  进行MD5加密
    @Override
    public String encode(CharSequence charSequence) {
        MD5.encrypt(charSequence.toString());
        return null;
    }
    //进行比对

    /**
     *
     * @param charSequence  传入的密码
     * @param encodingPassword 加密之后的密码
     * @return
     */
    @Override
    public boolean matches(CharSequence charSequence, String encodingPassword) {
        return encodingPassword.equals(MD5.encrypt(charSequence.toString()));
    }
}

TokenLogoutHandle 退出处理器

//退出处理器
public class TokenLogoutHandle implements LogoutHandler {
    //删除Token 根据token获取用户名在Redis里面进行删除
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenLogoutHandle(TokenManager tokenManager, RedisTemplate redisTemplate) {

        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        //在Header里面获取token
        // token不为空  移除token 从Redis删除token
        String token = request.getHeader("token");
        if (token!=null){
            //删除Token
            tokenManager.removeToken(token);
            //从Token获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            redisTemplate.delete(username);
        }
        ResponseUtil.out(response, R.ok());
    }
}

TokenManager token操作工具类

// token操作工具类
public class TokenManager {
    //token有效时长
    private long tokenEcpiration=24*60*60*100;
    //编码秘钥
    private String tokenSignKey="123456";
    //根据用户名生成Token
    public String createToken(String username){
        String token = Jwts.builder().setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration))
                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
        return token;
    }
    //根据Token得到用户信息
    public  String getUserInfoFromToken(String token){
        String userInfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
        return userInfo;
    }
    //删除Token
    public void removeToken(String token){}
}

UnauthorizedEntryPoint 未授权统一处理

//未授权统一处理类
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

TokenLoginFilter认证过滤器

//认证过滤  指定就是进行认证登录
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private AuthenticationManager authenticationManager;


    public TokenLoginFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) {
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.authenticationManager = authenticationManager;
        this.setPostOnly(false);
        //设置登录的路径和提交方式
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
    }
    //获取表单提交的用户名和密码
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),new ArrayList<>()));

        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    //认证成功调用的方法
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        // 认证成功  得到认证成功之后用户信息
        SecurityUser user = (SecurityUser)authResult.getPrincipal();
        //根据用户名生成token
        String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
        //将用户名和用户权限放到权限列表里面
        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
        //返回token
        ResponseUtil.out(response, R.ok().data("token",token));
    }
    //认证失败调用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        ResponseUtil.out(response,R.error());
    }
}

TokenAuthenticationFilter授权过滤器

//授权过滤
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, TokenManager tokenManager, RedisTemplate redisTemplate) {
        super(authenticationManager, authenticationEntryPoint);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //获取当前认证乘公共用户的授权信息
        //获取当前认证成功用户权限信息
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
        //判断如果有权限信息,放到权限上下文中
        if(authRequest != null) {
            SecurityContextHolder.getContext().setAuthentication(authRequest);
        }
        chain.doFilter(request,response);
    }
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){
        //从Header中获取Token
        String token = request.getHeader("token");
        if (token!=null){
            //从Token中获取用户名
            String username = tokenManager.getUserInfoFromToken(token);
            //从redis获取对应的权限列表
            List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
            Collection<GrantedAuthority> authority = new ArrayList<>();
            for(String permissionValue : permissionValueList) {
                SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
                authority.add(auth);
            }
            return new UsernamePasswordAuthenticationToken(username,token,authority);
        }
        return null;
    }
}

TokenWebSecurityConfig核心配置类

//核心配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;
    private DefaultPasswordEncoder defaultPasswordEncoder;
    private UserDetailsService userDetailsService;

    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
                                  TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }
    /**
     * 配置设置
     * @param http
     * @throws Exception
     */
    //设置退出的地址和token,redis操作地址
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint())//没有权限访问
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and().logout().logoutUrl("/admin/acl/index/logout")//退出路径
                .addLogoutHandler(new TokenLogoutHandle(tokenManager,redisTemplate)).and()
                .addFilter(new TokenLoginFilter( tokenManager, redisTemplate,authenticationManager()))
                .addFilter(new TokenAuthenticationFilter(authenticationManager(),tokenManager, redisTemplate)).httpBasic();
    }

    //调用userDetailsService和密码处理
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }
    //不进行认证的路径,可以直接访问
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/**");
    }
}

编写UserDetailServiceImpl

@Service("userDetailsService")  这个名字要与配合类里面定义的名字一致
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private PermissionService permissionService;  关于权限的

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询数据
        User user = userService.selectByUsername(username);
        //判断
        if(user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        User curUser = new User();
        
        BeanUtils.copyProperties(user,curUser);

        //根据用户查询用户权限列表
        List<String> permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());
        SecurityUser securityUser = new SecurityUser();
        securityUser.setCurrentUserInfo(curUser);
        securityUser.setPermissionValueList(permissionValueList);
        return securityUser;
    }
}

整体流程是

先进行认证: attemptAuthentication–>成功successfulAuthentication/失败unsuccessfulAuthentication–>在进行授权doFilterInternal
在这里插入图片描述

自我理解

在将前台的登录界面设置为不拦截的请求 前台界面登录执行登录请求之后 就会与Security的核心配置类里面设置的登录路径包含登录方法相匹配 如果匹配合格就会进行认证和授权的检测 检测是否能够进行登录并对其进行授权

其他具体相关代码在这里不在讲述 想要了解的请点击我的项目地址,如有不对的地方请指出公共学习
项目地址

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值