当前位置:   article > 正文

Java从坚持到精通-SpringSecurity_java安全框架怎么学习

java安全框架怎么学习

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.导入坐标

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-security</artifactId>
  4. </dependency>

2.启动项目

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

3.访问接口

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

4.SecurityProperties类分析

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

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

  1. spring:
  2. security:
  3. user:
  4. name: admin
  5. password: 123456

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

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

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

  1. @Configuration
  2. // 标记为一个Security类,启用SpringSecurity的自定义配置
  3. @EnableWebSecurity
  4. public class SecurityConfig {
  5. // 自定义用户名和密码
  6. @Bean
  7. public UserDetailsService userDetailsService(){
  8. // 定义用户信息
  9. UserDetails adminUser = User.withUsername("zhangsan")
  10. .password("{noop}111111")
  11. .roles("admin", "user")
  12. .build();
  13. UserDetails vipUser = User.withUsername("lisi")
  14. .password("{noop}111111")
  15. .roles("admin", "user")
  16. .build();
  17. // 将用户存储到SpringSecurity中
  18. InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
  19. // 创建两个用户
  20. userDetailsManager.createUser(adminUser);
  21. userDetailsManager.createUser(vipUser);
  22. return userDetailsManager;
  23. }
  24. }

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

5.密码加密处理

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

  1. @Bean
  2. public PasswordEncoder passwordEncoder(){
  3. // 构建密码编译器
  4. return new BCryptPasswordEncoder();
  5. }

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

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

  1. @SpringBootTest
  2. class SpringsecurityApplicationTests {
  3. @Autowired
  4. private PasswordEncoder passwordEncoder;
  5. @Test
  6. void contextLoads() {
  7. String password = "123456";
  8. String encode = passwordEncoder.encode(password);
  9. System.out.println("生成的密码为:"+encode);
  10. boolean matches = passwordEncoder.matches(password, encode);
  11. System.out.println("密码是否匹配:"+matches);
  12. }
  13. }

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

  1. @GetMapping("/getLoginUser1")
  2. public Authentication getLoginUser1(Authentication authentication){
  3. return authentication;
  4. }
  5. @GetMapping("/getLoginUser2")
  6. public Principal getLoginUser2(Principal principal){
  7. return principal;
  8. }
  9. @GetMapping("/getLoginUser3")
  10. public Principal getLoginUser3(){
  11. // 通过安全上下文持有器获取安全上下文,再获取认证信息
  12. return SecurityContextHolder.getContext().getAuthentication();
  13. }

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

7.权限和角色的问题

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

8.针对Url进行授权

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

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

9.针对方法进行授权

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

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

  1. @GetMapping("/getLoginUser1")
  2. @PreAuthorize("hasAuthority('admin')")
  3. public Authentication getLoginUser1(Authentication authentication){
  4. return authentication;
  5. }

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

1.登录成功

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

  1. @Resource
  2. private ObjectMapper objectMapper;
  3. @Override
  4. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
  5. HttpResult httpResult = HttpResult.builder()
  6. .code(1)
  7. .msg("登录成功")
  8. .build();
  9. String responseJson = objectMapper.writeValueAsString(httpResult);
  10. response.setContentType("application/json;charset=utf-8");
  11. PrintWriter writer = response.getWriter();
  12. writer.println(responseJson);
  13. writer.flush();
  14. }

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

 2.登录失败

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

3.退出登录

实现LogoutSuccessHandler接口

4.访问拒绝(没权限)

实现AccessDeniedHandler接口

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

11.基于数据库的认证

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

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

service、dao等,并且做好配置

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

  1. @Configuration
  2. @EnableGlobalMethodSecurity(prePostEnabled = true)
  3. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  4. @Bean
  5. public PasswordEncoder passwordEncoder(){
  6. // return new BCryptPasswordEncoder();
  7. // 测试时先用明文的
  8. return NoOpPasswordEncoder.getInstance();
  9. }
  10. @Override
  11. protected void configure(HttpSecurity http) throws Exception {
  12. http.authorizeRequests().anyRequest().authenticated();
  13. http.formLogin().permitAll();
  14. }
  15. }

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

  1. @Service
  2. public class SecurityUserDetailServiceImpl implements UserDetailsService {
  3. @Autowired
  4. private SysUserService sysUserService;
  5. @Override
  6. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  7. SysUser sysUser = sysUserService.getByUserName(username);
  8. if(sysUser == null){
  9. throw new UsernameNotFoundException("该用户不存在");
  10. }
  11. SecurityUser securityUser = new SecurityUser(sysUser);
  12. return securityUser;
  13. }
  14. }

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

  1. public class SecurityUser implements UserDetails {
  2. private final SysUser sysUser;
  3. public SecurityUser(SysUser sysUser){
  4. this.sysUser = sysUser;
  5. }
  6. @Override
  7. public Collection<? extends GrantedAuthority> getAuthorities() {
  8. return null;
  9. }
  10. @Override
  11. public String getPassword() {
  12. return sysUser.getPassword();
  13. }
  14. @Override
  15. public String getUsername() {
  16. return sysUser.getUsername();
  17. }
  18. @Override
  19. public boolean isAccountNonExpired() {
  20. return sysUser.getAccountNoExpired().equals(1);
  21. }
  22. @Override
  23. public boolean isAccountNonLocked() {
  24. return sysUser.getAccountNoLocked().equals(1);
  25. }
  26. @Override
  27. public boolean isCredentialsNonExpired() {
  28. return sysUser.getCredentialsNoExpired().equals(1);
  29. }
  30. @Override
  31. public boolean isEnabled() {
  32. return sysUser.getEnabled().equals(1);
  33. }
  34. }

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

12.基于数据库的授权

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

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

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

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

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

1.先引入thymeleaf的依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
  4. </dependency>

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

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

  1. @Controller
  2. public class PageController {
  3. // 跳转到登录页面
  4. @GetMapping("to_login")
  5. public String toLogin(){
  6. System.out.println("跳转到登录页面");
  7. return "login";
  8. }
  9. }

4.配置表单信息

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

5.配置退出信息

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

6.不跨域配置

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

14.集成图片验证码

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

  1. <dependency>
  2. <groupId>cn.hutool</groupId>
  3. <artifactId>hutool-all</artifactId>
  4. <version>5.8.7</version>
  5. </dependency>

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

  1. @Controller
  2. public class CaptchaController {
  3. @GetMapping("/code/image")
  4. public void getCaptchaCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
  5. CircleCaptcha circleCaptcha = CaptchaUtil.createCircleCaptcha(200, 100, 2, 20);
  6. String code = circleCaptcha.getCode();
  7. System.out.println("生成的图片验证码为:" + code);
  8. // 将验证码存储到session中
  9. request.getSession().setAttribute("CAPTCHA_CODE", code);
  10. ImageIO.write(circleCaptcha.getImage(), "jpeg", response.getOutputStream());
  11. }
  12. }

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

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

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

  1. @Component
  2. public class ValidateCodeFilter extends OncePerRequestFilter {
  3. @Override
  4. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  5. // 1.判断路径是否是/login/doLogin
  6. String requestURI = request.getRequestURI();
  7. // 如果不是登录请求,直接放行
  8. if(!requestURI.equals("/login/doLogin")){
  9. doFilter(request, response, filterChain);
  10. return;
  11. }
  12. validateCode(request, response, filterChain);
  13. }
  14. private void validateCode(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
  15. // 2.从前端获取验证码
  16. String enterCode = request.getParameter("code");
  17. // 3.从session中获取验证码
  18. String captchaCodeInSession = (String) request.getSession().getAttribute("CAPTCHA_CODE");
  19. // 4.判断二者是否相等
  20. if(!enterCode.equalsIgnoreCase(captchaCodeInSession)){
  21. request.getSession().setAttribute("captcha_code_error", "验证码输入错误");
  22. response.sendRedirect("/toLogin");
  23. return;
  24. }
  25. // 删除session中的验证码值
  26. request.getSession().removeAttribute("CAPTCHA_CODE");
  27. doFilter(request, response, filterChain);
  28. }
  29. }

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

 15.JWT

1.简介

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

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

2.组成

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

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

举例:

3.使用jwt

1.添加jwt的依赖

  1. <dependency>
  2. <groupId>com.auth0</groupId>
  3. <artifactId>java-jwt</artifactId>
  4. <version>3.18.3</version>
  5. </dependency>

2.创建工具类

  1. public class JwtUtils {
  2. // 密钥
  3. private static final String SECRET = "secret888";
  4. public String createJwt(Integer userId, String username, List<String> authList){
  5. Map<String ,Object> headerClaims = new HashMap<>();
  6. headerClaims.put("alg", "HS256");
  7. headerClaims.put("typ", "JWT");
  8. return JWT.create().withHeader(headerClaims) // 设置头部
  9. .withIssuer("duolaimi") // 设置签发人
  10. .withIssuedAt(new Date()) // 设置签发时间
  11. .withExpiresAt(new Date(new Date().getTime() + 1000*60*2)) // 设置两个小时过期
  12. .withClaim("userId", userId) // 自定义属性
  13. .withClaim("userName", username) // 自定义属性
  14. .withClaim("userAuth", authList) // 自定义属性
  15. .sign(Algorithm.HMAC256(SECRET));// 签名并指定密钥
  16. }
  17. public boolean verifyToken(String jwtToken){
  18. try {
  19. JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
  20. DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
  21. Integer userId = decodedJWT.getClaim("userId").asInt();
  22. return true;
  23. }catch (Exception e){
  24. System.out.println("token验证不正确!");
  25. return false;
  26. }
  27. }
  28. }

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/爱喝兽奶帝天荒/article/detail/773541
推荐阅读
相关标签
  

闽ICP备14008679号