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: adminpassword: 123456
如果对用户名和密码做了修改,控制台就不会输出密码信息了。
4.基于内存用户分析认证流程
我们需要定义一个配置类,并在类上加上@EnableWebSecurity注解,声明这是一个Security的配置类。我们需要new一个UserDetails对象,设置好用户名和密码,然后将这个对象放入到内存级的用户详情管理器中,启动项目即可。
@Configuration
// 标记为一个Security类,启用SpringSecurity的自定义配置
@EnableWebSecurity
public class SecurityConfig {// 自定义用户名和密码@Beanpublic 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.密码加密处理
我们可以在刚刚定义的配置类里面加上加密的配置
@Beanpublic PasswordEncoder passwordEncoder(){// 构建密码编译器return new BCryptPasswordEncoder();}
只需声明一个加密解析器的实现类,这里使用BCryptPasswordEncoder,比较常用。
然后我们测试的时候,只需加载这个密码解析器类,调用相应的加密方法和匹配方法即可(不可解密)
@SpringBootTest
class SpringsecurityApplicationTests {@Autowiredprivate PasswordEncoder passwordEncoder;@Testvoid 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然后输出。
@Resourceprivate ObjectMapper objectMapper;@Overridepublic 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 {@Beanpublic PasswordEncoder passwordEncoder(){// return new BCryptPasswordEncoder();// 测试时先用明文的return NoOpPasswordEncoder.getInstance();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated();http.formLogin().permitAll();}
}
4.创建一个实现类实现UserDetailService
@Service
public class SecurityUserDetailServiceImpl implements UserDetailsService {@Autowiredprivate SysUserService sysUserService;@Overridepublic 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;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return sysUser.getPassword();}@Overridepublic String getUsername() {return sysUser.getUsername();}@Overridepublic boolean isAccountNonExpired() {return sysUser.getAccountNoExpired().equals(1);}@Overridepublic boolean isAccountNonLocked() {return sysUser.getAccountNoLocked().equals(1);}@Overridepublic boolean isCredentialsNonExpired() {return sysUser.getCredentialsNoExpired().equals(1);}@Overridepublic 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 {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 1.判断路径是否是/login/doLoginString 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()指定加密方式。