赞
踩
安全框架的本质就是一堆过滤器的组成,目的在于保护系统资源,所以在到达资源之前会做一系列的验证工作,这些验证工作通过一系列的过滤器完成。安全框架通常的功能有认证、授权、防止常见的网络攻击,以此为核心拓展其他功能。比如session管理,密码加密,权限管理等功能。
shiro是Apache下的一个开源安全框架,提供了身份验证、授权、密码学和会话管理等关于安全的核心功能。
SpringSecurity底层主要是基于Spring AOP和Servlet过滤器来实现安全控制,它提供了全面的安全解决方案,同时授权粒度可以在web请求级和方法调用级来处理身份确认和授权。
SpringSecurity的核心功能主要包括以下几个:
是一款国产安全框架,使用简单,轻便。文档清晰详细,内置多重功能。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
启动项目之后,我们会发现控制台会输出一串密码,然后后面有很多过滤器的加载。
访问接口会默认跳转到登录页,默认的用户名是user,然后密码就是之前在控制台输出的密码。
如果我们没有指定用户名和密码,则默认会使用SecurityProperties里的User类生成默认用户名和密码,user和uuid生成的字符串。
然后类上使用了@ConfigurationProperties(prefix = "spring.security")注解说明了配置的对应关系,如果我们需要修改默认配置,则按照spring.security开始修改即可。
- spring:
- security:
- user:
- name: admin
- password: 123456
如果对用户名和密码做了修改,控制台就不会输出密码信息了。
我们需要定义一个配置类,并在类上加上@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出一个。
我们可以在刚刚定义的配置类里面加上加密的配置
- @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);
- }
-
- }
- @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对象后,会将这个对象再放入得到安全上下文对象中,然后从安全上下文持有器中获取认证信息。
我们可以在配置类中定义用户的角色(role)和权限(authority),角色和权限在这里是一个意思,设置角色的时候,获取角色会在角色前面拼上“ROLE_”,而且角色和权限,谁在下面谁就会生效(覆盖了前面的)。
首先,原来的配置类需要继承WebSecurityConfigurerAdapter抽象类,然后重写里面的configure方法
我们可以定义路径和权限的匹配规则,你访问某个路径时,需要查看你对应的权限,如果没有权限则无法访问,如果访问了这里面没有的配置路径,则不需要权限。
首先要在配置类上面加上@EnableGlobalMethodSecurity(prePostEnabled = true)这个注解,说明要开启全局方法安全。
然后在方法上使用注解完成。
- @GetMapping("/getLoginUser1")
- @PreAuthorize("hasAuthority('admin')")
- public Authentication getLoginUser1(Authentication authentication){
- return authentication;
- }
定义的配置类需要实现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)
与上面登录成功一样的配置,只是要实现AuthenticationFailureHandler这个接口。
实现LogoutSuccessHandler接口
实现AccessDeniedHandler接口
然后在原有的配置方法中按如上所示调用。
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会显得很长很臃肿,所以我们就采用构造方法中属性赋值的方法。
1.我们根据sys_menu创建对应的实体类,service以及serviceImpl。
2.编写查询的核心sql语句(三表联查)
3.在SecurityUser类中新增权限集合属性,这里我们加上了@Data,所以不需要写权限集合的set方法
4.根据userId查询到用户的所有权限,并且设置到List<SimpleGrantedAuthority>集合中,然后设置权限集合即可
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- @Controller
- public class PageController {
-
- // 跳转到登录页面
- @GetMapping("to_login")
- public String toLogin(){
- System.out.println("跳转到登录页面");
- return "login";
- }
-
- }
在http.formLogin()...后面配置登录相关的配置,比如登录的页面,登录的用户名密码,登录请求的接口,登录失败和成功的路径等。
在如上所示图中,可以通过http.logout....配置退出成功的路径。
禁用csrf,使用http.csrf().disabled()来禁用跨域,否则他们校验token,导致登录无法通过。
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.最后,注入我们定义好的验证码过滤器,并将这个过滤器加到用户名密码过滤器之前执行。
jwt是Jason Web Token的缩写,用于网络安全传输,是一种好的传输方式。
jwt就是一个加密的带用户信息的字符串。
一个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()指定加密方式。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。