SpringBoot + Spring Cloud +Vue 管理系统后台搭建(九、用户认证和授权)

安全框架我们使用JWT(Json Web Token)和Spring Security

JWT官网:https://jwt.io/introduction

Spring Security官网:https://spring.io/projects/spring-security

Spring Security教程:https://www.w3cschool.cn/springsecurity/

第一步、添加pom依赖

在mango-admin的pom文件中添加Spring Security 和 JWT 依赖包

<!--   Spring Security    -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
   <version>2.4.4</version>
</dependency>

<!--   JWT    -->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.1</version>
</dependency>

第二步、添加配置类

新建security包

登录认证过滤器

在包下添加登录认证过滤器JwtAuthenticationFilter 并继承 BasicAuthenticationFilter

重写doFilterInternal进行token校验

/**
 * 登录认证过滤器
 * @author Louis
 * @date Jan 14, 2019
 */
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

   
   @Autowired
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
       // 获取token, 并检查登录状态
        SecurityUtils.checkAuthentication(request);
        chain.doFilter(request, response);
    }
    
}

我们把验证逻辑放到SecurityUtils 的checkAuthentication方法中,checkAuthentication通过JwtTokenUtils的方法获取认证信息并保存到SpringSecurity上下文

JwtTokenUtils.java
/**
 * JWT工具类
 * @author Louis
 * @date Jan 14, 2019
 */
public class JwtTokenUtils implements Serializable {

   private static final long serialVersionUID = 1L;
   
   /**
    * 用户名称
    */
   private static final String USERNAME = Claims.SUBJECT;
   /**
    * 创建时间
    */
   private static final String CREATED = "created";
   /**
    * 权限列表
    */
   private static final String AUTHORITIES = "authorities";
   /**
     * 密钥
     */
    private static final String SECRET = "abcdefgh";
    /**
     * 有效期12小时
     */
    private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;

    /**
    * 生成令牌
    *
    * @param authentication 用户
    * @return 令牌
    */
   public static String generateToken(Authentication authentication) {
       Map<String, Object> claims = new HashMap<>(3);
       claims.put(USERNAME, SecurityUtils.getUsername(authentication));
       claims.put(CREATED, new Date());
       claims.put(AUTHORITIES, authentication.getAuthorities());
       return generateToken(claims);
   }

   /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private static String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
    }

    /**
    * 从令牌中获取用户名
    *
    * @param token 令牌
    * @return 用户名
    */
   public static String getUsernameFromToken(String token) {
       String username;
       try {
           Claims claims = getClaimsFromToken(token);
           username = claims.getSubject();
       } catch (Exception e) {
           username = null;
       }
       return username;
   }
   
   /**
    * 根据请求令牌获取登录认证信息
    * @param  request
    * @return 用户名
    */
   public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
      Authentication authentication = null;
      // 获取请求携带的令牌
      String token = JwtTokenUtils.getToken(request);
      if(token != null) {
         // 请求令牌不能为空
         if(SecurityUtils.getAuthentication() == null) {
            // 上下文中Authentication为空
            Claims claims = getClaimsFromToken(token);
            if(claims == null) {
               return null;
            }
            String username = claims.getSubject();
            if(username == null) {
               return null;
            }
            if(isTokenExpired(token)) {
               return null;
            }
            Object authors = claims.get(AUTHORITIES);
            List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
            if (authors != null && authors instanceof List) {
               for (Object object : (List) authors) {
                  authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
               }
            }
            authentication = new JwtAuthenticatioToken(username, null, authorities, token);
         } else {
            if(validateToken(token, SecurityUtils.getUsername())) {
               // 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
               authentication = SecurityUtils.getAuthentication();
            }
         }
      }
      return authentication;
   }

   /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
    * 验证令牌
    * @param token
    * @param username
    * @return
    */
   public static Boolean validateToken(String token, String username) {
       String userName = getUsernameFromToken(token);
       return (userName.equals(username) && !isTokenExpired(token));
   }

   /**
    * 刷新令牌
    * @param token
    * @return
    */
   public static String refreshToken(String token) {
       String refreshedToken;
       try {
           Claims claims = getClaimsFromToken(token);
           claims.put(CREATED, new Date());
           refreshedToken = generateToken(claims);
       } catch (Exception e) {
           refreshedToken = null;
       }
       return refreshedToken;
   }

   /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public static Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 获取请求token
     * @param request
     * @return
     */
    public static String getToken(HttpServletRequest request) {
       String token = request.getHeader("Authorization");
        String tokenHead = "Bearer ";
        if(token == null) {
           token = request.getHeader("token");
        } else if(token.contains(tokenHead)){
           token = token.substring(tokenHead.length());
        } 
        if("".equals(token)) {
           token = null;
        }
        return token;
    }

}

 

SecurityUtils .java

/**
 * Security相关操作
 * @author Louis
 * @date Jan 14, 2019
 */
public class SecurityUtils {

   /**
    * 系统登录认证
    * @param request
    * @param username
    * @param password
    * @param authenticationManager
    * @return
    */
   public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
      JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
      token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
      // 执行登录认证过程
       Authentication authentication = authenticationManager.authenticate(token);
       // 认证成功存储认证信息到上下文
       SecurityContextHolder.getContext().setAuthentication(authentication);
      // 生成令牌并返回给客户端
       token.setToken(JwtTokenUtils.generateToken(authentication));
      return token;
   }

   /**
    * 获取令牌进行认证
    * @param request
    */
   public static void checkAuthentication(HttpServletRequest request) {
      // 获取令牌并根据令牌获取登录认证信息
      Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
      // 设置登录认证信息到上下文
      SecurityContextHolder.getContext().setAuthentication(authentication);
   }

   /**
    * 获取当前用户名
    * @return
    */
   public static String getUsername() {
      String username = null;
      Authentication authentication = getAuthentication();
      if(authentication != null) {
         Object principal = authentication.getPrincipal();
         if(principal != null && principal instanceof UserDetails) {
            username = ((UserDetails) principal).getUsername();
         }
      }
      return username;
   }
   
   /**
    * 获取用户名
    * @return
    */
   public static String getUsername(Authentication authentication) {
      String username = null;
      if(authentication != null) {
         Object principal = authentication.getPrincipal();
         if(principal != null && principal instanceof UserDetails) {
            username = ((UserDetails) principal).getUsername();
         }
      }
      return username;
   }
   
   /**
    * 获取当前登录信息
    * @return
    */
   public static Authentication getAuthentication() {
      if(SecurityContextHolder.getContext() == null) {
         return null;
      }
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      return authentication;
   }
   
}

在config包下添加Spring Security配置类WebSecurityConfig.java

主要进行安全相关配置,如URL匹配策略、认证过滤、开启权限认证注解等

/**
 * Spring Security配置
 * @author Louis
 * @date Jan 14, 2019
 */
@Configuration
@EnableWebSecurity // 开启Spring Security 
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,如:@PreAuthorize注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 使用自定义身份验证组件
        auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
        http.cors().and().csrf().disable()
          .authorizeRequests()
          // 跨域预检请求
            .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            // web jars
            .antMatchers("/webjars/**").permitAll()
            // 查看SQL监控(druid)
            .antMatchers("/druid/**").permitAll()
            // 首页和登录页面
            .antMatchers("/").permitAll()
            .antMatchers("/login").permitAll()
            // swagger
            .antMatchers("/swagger-ui.html").permitAll()
            .antMatchers("/swagger-resources/**").permitAll()
            .antMatchers("/v2/api-docs").permitAll()
            .antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
            // 验证码
            .antMatchers("/captcha.jpg**").permitAll()
            // 服务监控
            .antMatchers("/actuator/**").permitAll()
            // 其他所有请求需要身份认证
            .anyRequest().authenticated();
        // 退出登录处理器
        http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
        // token验证过滤器
        http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManager() throws Exception {
       return super.authenticationManager();
    }
    
}

身份验证类

在Security包下新建JwtAuthenticationProvider类并继承DaoAuthenticationProvider

重写additionalAuthenticationChecks方法来进行密码匹配,我们这里没有使用默认的密码认证器(我们使用的盐salt来对密码加密,默认密码认证器没有加盐)

JwtAuthenticationProvider.java

/**
 * 身份验证提供者
 * @author Louis
 * @date Jan 14, 2019
 */
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {

    public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
        setUserDetailsService(userDetailsService);
    }

    @Override
   protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
         throws AuthenticationException {
      if (authentication.getCredentials() == null) {
         logger.debug("Authentication failed: no credentials provided");
         throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
      }

      String presentedPassword = authentication.getCredentials().toString();
      String salt = ((JwtUserDetails) userDetails).getSalt();
      // 覆写密码验证逻辑
      if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
         logger.debug("Authentication failed: password does not match stored value");
         throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
      }
   }

}

认证信息查询

在security包下新建UserDetailsServiceImpl类实现UserDetailsService

重新其中的loadUserByUsername,查询用户的密码和权限信息封装到JwtUserDetails

JwtUserDetails类需要实现UserDetails,是对认证信息的封装,包含用户名、密码、加密盐和权限信息

JwtUserDetails.java

/**
 * 安全用户模型
 * @author Louis
 * @date Jan 14, 2019
 */
public class JwtUserDetails implements UserDetails {

   private static final long serialVersionUID = 1L;
   
   private String username;
    private String password;
    private String salt;
    private Collection<? extends GrantedAuthority> authorities;

    JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.salt = salt;
        this.authorities = authorities;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

    public String getSalt() {
      return salt;
   }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}
UserDetailsServiceImpl.java
/**
 * 用户登录认证信息查询
 * @author Louis
 * @date Jan 14, 2019
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = sysUserService.findByName(username);
        if (user == null) {
            throw new UsernameNotFoundException("该用户不存在");
        }
        // 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
        Set<String> permissions = sysUserService.findPermissions(user.getName());
        List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
        return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
    }
}

权限分装类

在security包下新建GrantedAuthorityImpl类实现GrantedAuthority是对权限的封装,内部包含一个字符串类型的权限标识authority,

对应菜单表的perms字段的权限字符串,比如用户管理新增权限标志sys:user:add

/**
 * 权限封装
 * @author Louis
 * @date Jan 14, 2019
 */
public class GrantedAuthorityImpl implements GrantedAuthority {
   
   private static final long serialVersionUID = 1L;

   private String authority;

    public GrantedAuthorityImpl(String authority) {
        this.authority = authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }

    @Override
    public String getAuthority() {
        return this.authority;
    }
}

 

添加权限注解

spring security提供了权限注解@PreAuthorize来保护后台接口免受非法访问

这里我们以字典模块为例,其他模块同理

SysDictController.java
/**
 * 字典控制器
 * @author Louis
 * @date Jan 13, 2019
 */
@RestController
@RequestMapping("dict")
public class SysDictController {

   @Autowired
   private SysDictService sysDictService;
   //用户必须同事拥有添加和修改权限才有权限
   @PreAuthorize("hasAuthority('sys:dict:add') AND hasAuthority('sys:dict:edit')")
   @PostMapping(value="/save")
   public HttpResult save(@RequestBody SysDict record) {
      return HttpResult.ok(sysDictService.save(record));
   }

   @PreAuthorize("hasAuthority('sys:dict:delete')")
   @PostMapping(value="/delete")
   public HttpResult delete(@RequestBody List<SysDict> records) {
      return HttpResult.ok(sysDictService.delete(records));
   }

   @PreAuthorize("hasAuthority('sys:dict:view')")
   @PostMapping(value="/findPage")
   public HttpResult findPage(@RequestBody PageRequest pageRequest) {
      return HttpResult.ok(sysDictService.findPage(pageRequest));
   }
   
   @PreAuthorize("hasAuthority('sys:dict:view')")
   @GetMapping(value="/findByLable")
   public HttpResult findByLable(@RequestParam String lable) {
      return HttpResult.ok(sysDictService.findByLable(lable));
   }
}

Swagger添加令牌参数

由于我们引入的spring security 接口受到保护,需要有合法的token才能正常访问,swagger本身默认并没有,因此需要修改一些之前SwaggerConfig配置类,修改后代码

@EnableSwagger2
@Configuration
public class SwaggerConfig {
    @Bean
    public Docket createRestApi(){
        // 添加请求参数,我们这里把token作为请求头部参数传入后端
        ParameterBuilder parameterBuilder = new ParameterBuilder();
        List<Parameter> parameters = new ArrayList<Parameter>();
        parameterBuilder.name("token").description("令牌")
                .modelRef(new ModelRef("string")).parameterType("header").required(false).build();
        parameters.add(parameterBuilder.build());
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
                .build().globalOperationParameters(parameters);
        //return  new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder().build();
    }
}

配置完启动项目,访问swagger就可以传参了

 

登录接口实现

在登录控制器中添加login接口,在其中验证验证码、用户名、密码信息,匹配成功后执行spring security的登录认证机制,登录成功后返回Token令牌凭证

/**
 * 登录接口
 */
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
   String username = loginBean.getAccount();
   String password = loginBean.getPassword();
   String captcha = loginBean.getCaptcha();
   // 从session中获取之前保存的验证码跟前台传来的验证码进行匹配
   Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
   if(kaptcha == null){
      return HttpResult.error("验证码已失效");
   }
   if(!captcha.equals(kaptcha)){
      return HttpResult.error("验证码不正确");
   }
   // 用户信息
   SysUser user = sysUserService.findByName(username);
   // 账号不存在、密码错误
   if (user == null) {
      return HttpResult.error("账号不存在");
   }
   if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) {
      return HttpResult.error("密码不正确");
   }
   // 账号锁定
   if (user.getStatus() == 0) {
      return HttpResult.error("账号已被锁定,请联系管理员");
   }
   // 系统登录认证
   JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
   return HttpResult.ok(token);
}

登录认证逻辑我们已经封装到了上面的SecurityUtils的login方法中,认证流程大体分为以下4步

1、将用户名密码的认证信息封装到JwtAuthenticatioToken对象

2、通过调用authenticationManager.authenticate(token)执行认证流程

3、通过SecurityContextHolder将认证信息保存到Security上下文

4、JwtTokenUtils.generateToken(authentication)生成token并返回

 

新建LoginBean.java 

对登录认证信息简单封装

/**
 * 登录接口封装对象
 * @author Louis
 * @date Oct 29, 2018
 */
public class LoginBean {

   private String account;
   private String password;
   private String captcha;
   
   public String getAccount() {
      return account;
   }
   public void setAccount(String account) {
      this.account = account;
   }
   public String getPassword() {
      return password;
   }
   public void setPassword(String password) {
      this.password = password;
   }
   public String getCaptcha() {
      return captcha;
   }
   public void setCaptcha(String captcha) {
      this.captcha = captcha;
   }
   
}

令牌自定义

在Security包下新建JwtAuthenticatioToken.java继承UsernamePasswordAuthenticationToken

对令牌信息简单封装,用来作为认证和授权的信息凭证,其中token信息有JWT生成

/**
 * 自定义令牌对象
 * @author Louis
 * @date Jan 14, 2019
 */
public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {

   private static final long serialVersionUID = 1L;
   
   private String token;

    public JwtAuthenticatioToken(Object principal, Object credentials){
        super(principal, credentials);
    }
    
    public JwtAuthenticatioToken(Object principal, Object credentials, String token){
       super(principal, credentials);
       this.token = token;
    }

    public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
       super(principal, credentials, authorities);
       this.token = token;
    }
    
   public String getToken() {
      return token;
   }

   public void setToken(String token) {
      this.token = token;
   }

   public static long getSerialversionuid() {
      return serialVersionUID;
   }

}

到此我们已经完成配置下面进行Swagger测试

先执行验证码生成接口获取验证码

 

然后执行登录接口,输入用户名和密码和刚生成的验证码,登录成功后返回token

 

在调用其他接口不输入token,直接403,说明我们安全校验生效

 

 

输入token在调用,可以看到执行成功

 

希望可以帮助大家,记得点赞哦

 

 

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值