Java从坚持到精通-SpringSecurity

1.安全框架是什么

安全框架的本质就是一堆过滤器的组成,目的在于保护系统资源,所以在到达资源之前会做一系列的验证工作,这些验证工作通过一系列的过滤器完成。安全框架通常的功能有认证、授权、防止常见的网络攻击,以此为核心拓展其他功能。比如session管理,密码加密,权限管理等功能。

2.常见的安全框架比较

Shiro

shiro是Apache下的一个开源安全框架,提供了身份验证、授权、密码学和会话管理等关于安全的核心功能。

SpringSecurity

SpringSecurity底层主要是基于Spring AOP和Servlet过滤器来实现安全控制,它提供了全面的安全解决方案,同时授权粒度可以在web请求级和方法调用级来处理身份确认和授权。

SpringSecurity的核心功能主要包括以下几个:

  • 认证:解决“你是谁”的问题->解决的是系统中是否有这个“用户”(用户/设备/系统)的问题,也就是我们常说的“登录”
  • 授权:权限控制/鉴别,解决的是系统中某个用户能够访问哪些资源,即“你能干什么”的问题。Spring Security支持基于URL的请求授权、方法访问授权、对象访问授权。
  • 防护攻击:防止身份伪造等各种攻击手段
  • 加密功能:对密码进行加密、匹配等
  • 会话功能:对Session进行管理
  • RememberMe功能:实现“记住我”功能,并可以实现token令牌持久化。

SpringSecurity与Shiro两者区别

  • SpringSecurity基于Spring开发,与SpringBoot、SpringCloud更容易集成
  • SpringSecurity拥有更多功能,如安全防护,对OAuth授权登录的支持
  • SpringSecurity拥有良好的扩展性,更容易自定义实现一些定制需求
  • SpringSecurity的社区资源比Shiro更丰富
  • Shiro相较于SpringSecurity更轻便,简单,使用流程更清晰,上手容易,反观SpringSecurity属于重量级,学习难度比Shiro高
  • Shiro不依赖其他框架可独立运行,而SpringSecurity需要已离开与Spring容器运行

Sa-Token

是一款国产安全框架,使用简单,轻便。文档清晰详细,内置多重功能。

3.使用SpringSecurity

1.导入坐标

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

2.启动项目

启动项目之后,我们会发现控制台会输出一串密码,然后后面有很多过滤器的加载。

3.访问接口

访问接口会默认跳转到登录页,默认的用户名是user,然后密码就是之前在控制台输出的密码。

4.SecurityProperties类分析

如果我们没有指定用户名和密码,则默认会使用SecurityProperties里的User类生成默认用户名和密码,user和uuid生成的字符串。

然后类上使用了@ConfigurationProperties(prefix = "spring.security")注解说明了配置的对应关系,如果我们需要修改默认配置,则按照spring.security开始修改即可。

spring:
  security:
    user:
      name: admin
      password: 123456

如果对用户名和密码做了修改,控制台就不会输出密码信息了。

4.基于内存用户分析认证流程

我们需要定义一个配置类,并在类上加上@EnableWebSecurity注解,声明这是一个Security的配置类。我们需要new一个UserDetails对象,设置好用户名和密码,然后将这个对象放入到内存级的用户详情管理器中,启动项目即可。

@Configuration
// 标记为一个Security类,启用SpringSecurity的自定义配置
@EnableWebSecurity
public class SecurityConfig {

    // 自定义用户名和密码
    @Bean
    public UserDetailsService userDetailsService(){
        // 定义用户信息
        UserDetails adminUser = User.withUsername("zhangsan")
                .password("{noop}111111")
                .roles("admin", "user")
                .build();

        UserDetails vipUser = User.withUsername("lisi")
                .password("{noop}111111")
                .roles("admin", "user")
                .build();

        // 将用户存储到SpringSecurity中
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        // 创建两个用户
        userDetailsManager.createUser(adminUser);
        userDetailsManager.createUser(vipUser);

        return userDetailsManager;
    }

}

 具体他会执行到loadUserByUsername这个方法中,查看用户名和密码,有的话就new出一个。

5.密码加密处理

我们可以在刚刚定义的配置类里面加上加密的配置

    @Bean
    public PasswordEncoder passwordEncoder(){
        // 构建密码编译器
        return new BCryptPasswordEncoder();
    }

只需声明一个加密解析器的实现类,这里使用BCryptPasswordEncoder,比较常用。

然后我们测试的时候,只需加载这个密码解析器类,调用相应的加密方法和匹配方法即可(不可解密)

@SpringBootTest
class SpringsecurityApplicationTests {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    void contextLoads() {
        String password = "123456";
        String encode = passwordEncoder.encode(password);
        System.out.println("生成的密码为:"+encode);

        boolean matches = passwordEncoder.matches(password, encode);
        System.out.println("密码是否匹配:"+matches);
    }

}

6.获取登录用户信息的方法

    @GetMapping("/getLoginUser1")
    public Authentication getLoginUser1(Authentication authentication){
        return authentication;
    }

    @GetMapping("/getLoginUser2")
    public Principal getLoginUser2(Principal principal){
        return principal;
    }

    @GetMapping("/getLoginUser3")
    public Principal getLoginUser3(){
        // 通过安全上下文持有器获取安全上下文,再获取认证信息
        return SecurityContextHolder.getContext().getAuthentication();
    }

 以上三个对象Authentication继承了Principal,先返回Authentication对象后,会将这个对象再放入得到安全上下文对象中,然后从安全上下文持有器中获取认证信息。

7.权限和角色的问题

我们可以在配置类中定义用户的角色(role)和权限(authority),角色和权限在这里是一个意思,设置角色的时候,获取角色会在角色前面拼上“ROLE_”,而且角色和权限,谁在下面谁就会生效(覆盖了前面的)。

8.针对Url进行授权

首先,原来的配置类需要继承WebSecurityConfigurerAdapter抽象类,然后重写里面的configure方法

我们可以定义路径和权限的匹配规则,你访问某个路径时,需要查看你对应的权限,如果没有权限则无法访问,如果访问了这里面没有的配置路径,则不需要权限。

9.针对方法进行授权

首先要在配置类上面加上@EnableGlobalMethodSecurity(prePostEnabled = true)这个注解,说明要开启全局方法安全。

然后在方法上使用注解完成。

    @GetMapping("/getLoginUser1")
    @PreAuthorize("hasAuthority('admin')")
    public Authentication getLoginUser1(Authentication authentication){
        return authentication;
    }

10.登录成功或者失败返回json

1.登录成功

定义的配置类需要实现AuthenticationSuccessHandler接口,然后重写onAuthenticationSuccess方法,这里需要引入ObjectMapper,将字符串转成json然后输出。

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HttpResult httpResult = HttpResult.builder()
                .code(1)
                .msg("登录成功")
                .build();

        String responseJson = objectMapper.writeValueAsString(httpResult);

        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(responseJson);
        writer.flush();
    }

然后在原有设置了放行的方法中添加successHandler方法,将上面那个对象作为参数传进去(我这里因为是写在一个类当中,所以用了this)

 2.登录失败

与上面登录成功一样的配置,只是要实现AuthenticationFailureHandler这个接口。

3.退出登录

实现LogoutSuccessHandler接口

4.访问拒绝(没权限)

实现AccessDeniedHandler接口

然后在原有的配置方法中按如上所示调用。

11.基于数据库的认证

1.我们需要新建基本的5张表

2.我们后端使用mybatis,所以创建好基本的框架

service、dao等,并且做好配置

3.新建配置类,继承WebSecurityConfigurerAdapter,重写里面的configure方法。并在这里设置好密码的加密方式,我这里方便测试,直接配成明文的了,对应数据表中的数据也是明文存储的。

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        // return new BCryptPasswordEncoder();
        // 测试时先用明文的
        return NoOpPasswordEncoder.getInstance();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
        http.formLogin().permitAll();
    }
}

4.创建一个实现类实现UserDetailService

@Service
public class SecurityUserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getByUserName(username);

        if(sysUser == null){
            throw new UsernameNotFoundException("该用户不存在");
        }


        SecurityUser securityUser = new SecurityUser(sysUser);

        return securityUser;
    }
}

 解析,注意,这里是springsecurity的核心判断登录的方法,他在登录成功之后,需要返回UserDails对象,所以我们创建了一个SecurityUser对象来实现UserDails,就可以正常返回了。在SecurityUser中我们定义一个属性SysUser,就是数据表中的sys_user对应的对象,将数据表中的数据通过构造方法进行传参并赋值springsecurity里的属性。

public class SecurityUser implements UserDetails {

    private final SysUser sysUser;

    public SecurityUser(SysUser sysUser){
        this.sysUser = sysUser;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    @Override
    public String getUsername() {
        return sysUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return sysUser.getAccountNoExpired().equals(1);
    }

    @Override
    public boolean isAccountNonLocked() {
        return sysUser.getAccountNoLocked().equals(1);
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return sysUser.getCredentialsNoExpired().equals(1);
    }

    @Override
    public boolean isEnabled() {
        return sysUser.getEnabled().equals(1);
    }
}

 这里其实还有一种方法,是SysUser直接实现UserDetails接口,但是这样SysUser会显得很长很臃肿,所以我们就采用构造方法中属性赋值的方法。

12.基于数据库的授权

1.我们根据sys_menu创建对应的实体类,service以及serviceImpl。

2.编写查询的核心sql语句(三表联查)

3.在SecurityUser类中新增权限集合属性,这里我们加上了@Data,所以不需要写权限集合的set方法

4.根据userId查询到用户的所有权限,并且设置到List<SimpleGrantedAuthority>集合中,然后设置权限集合即可

13.自定义登录界面(先不跨域)

1.先引入thymeleaf的依赖

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

 2.然后在templates目录下新建文件

3.创建控制器,指定跳转路径

@Controller
public class PageController {

    // 跳转到登录页面
    @GetMapping("to_login")
    public String toLogin(){
        System.out.println("跳转到登录页面");
        return "login";
    }

}

4.配置表单信息

在http.formLogin()...后面配置登录相关的配置,比如登录的页面,登录的用户名密码,登录请求的接口,登录失败和成功的路径等。

5.配置退出信息

在如上所示图中,可以通过http.logout....配置退出成功的路径。

6.不跨域配置

禁用csrf,使用http.csrf().disabled()来禁用跨域,否则他们校验token,导致登录无法通过。

14.集成图片验证码

1.先加入hutool的依赖,里面可以使用验证码工具类相关方法

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.7</version>
        </dependency>

2.创建一个验证码控制器,用于生成验证码,这里使用hutool的验证码工具类来生成验证码,并将生成的验证码放在session中,然后使用ImageIO类来返回验证码图片给前端

@Controller
public class CaptchaController {

    @GetMapping("/code/image")
    public void getCaptchaCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);

        String code = circleCaptcha.getCode();

        System.out.println("生成的图片验证码为:" + code);

        // 将验证码存储到session中
        request.getSession().setAttribute("CAPTCHA_CODE", code);

        ImageIO.write(circleCaptcha.getImage(), "jpeg", response.getOutputStream());
    }


}

3.在springsecurity的主配置类中添加上验证码的请求路径,说明请求验证码是需要放行的

 4.前端登录页需要指定验证码的name和请求路径

 5.创建一个过滤器,需要继承OncePerRequestFilter抽象类,然后重写doFilterInternal方法,这里判断请求的路径,只有登录的接口需要验证码,别的接口直接放行。然后如果是登录接口,还需要校验验证码。

@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1.判断路径是否是/login/doLogin
        String requestURI = request.getRequestURI();
        // 如果不是登录请求,直接放行
        if(!requestURI.equals("/login/doLogin")){
            doFilter(request, response, filterChain);
            return;
        }

        validateCode(request, response, filterChain);

    }

    private void validateCode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        // 2.从前端获取验证码
        String enterCode = request.getParameter("code");

        // 3.从session中获取验证码
        String captchaCodeInSession = (String) request.getSession().getAttribute("CAPTCHA_CODE");

        // 4.判断二者是否相等
        if(!enterCode.equalsIgnoreCase(captchaCodeInSession)){
            request.getSession().setAttribute("captcha_code_error", "验证码输入错误");
            response.sendRedirect("/toLogin");
            return;
        }

        // 删除session中的验证码值
        request.getSession().removeAttribute("CAPTCHA_CODE");

        doFilter(request, response, filterChain);

    }
}

6.最后,注入我们定义好的验证码过滤器,并将这个过滤器加到用户名密码过滤器之前执行。

 15.JWT

1.简介

jwt是Jason Web Token的缩写,用于网络安全传输,是一种好的传输方式。

jwt就是一个加密的带用户信息的字符串。

2.组成

一个jwt由三部分组成,各部分以点分隔:

  • Header(头部):base64Url编码的Json字符串
  • Playload(载荷):base64Url编码的Json字符串
  • Signature(签名):使用指定算法,通过Header和Payload加盐计算的字符串

举例:

3.使用jwt

1.添加jwt的依赖

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.3</version>
        </dependency>

2.创建工具类

public class JwtUtils {

    // 密钥
    private static final String SECRET = "secret888";

    public String createJwt(Integer userId, String username, List<String> authList){
        Map<String ,Object> headerClaims = new HashMap<>();
        headerClaims.put("alg", "HS256");
        headerClaims.put("typ", "JWT");
        return JWT.create().withHeader(headerClaims) // 设置头部
                .withIssuer("duolaimi") // 设置签发人
                .withIssuedAt(new Date()) // 设置签发时间
                .withExpiresAt(new Date(new Date().getTime() + 1000*60*2)) // 设置两个小时过期
                .withClaim("userId", userId) // 自定义属性
                .withClaim("userName", username) // 自定义属性
                .withClaim("userAuth", authList) // 自定义属性
                .sign(Algorithm.HMAC256(SECRET));// 签名并指定密钥
    }

    public boolean verifyToken(String jwtToken){
        try {
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
            Integer userId = decodedJWT.getClaim("userId").asInt();
            return true;
        }catch (Exception e){
            System.out.println("token验证不正确!");
            return false;
        }
    }

}

这个工具类主要有两个方法,一个是创建jwt字符串,一个是验证jwt字符串。

创建jwt字符串时,可以使用JWT.create()方法,然后后面指定头部、签发人、签发时间等必要信息。

验证jwt时,需要使用JWT.require()指定加密方式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值