springsecurity+JWT前后端分离(基础篇)

首先要了解一下sercurity的工作原理
建议拜读一下大佬文章
Spring Security 工作原理概览
在这里插入图片描述

借用一下大佬的图
首先明确一下我们要实现的功能

  1. 第一次账号密码成功认证登录后,服务器要向客户端的响应头里要设置我们生成的token,下发token
  2. 客户端在第一次登录后访问其他api接口时,需要在请求头里带上服务器下发的token才能允许访问接口

我们先来实现第一个功能
首先肯定是要导入jar包了

 <!--安全验证-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!--jwt-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
           <!--hutool-->
        <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.7.22</version>
        </dependency>

hutool是一个工具类,不想用也可以没必要

说明引入security的jar包后再启动项目默认会有个登录页面
在这里插入图片描述

账号默认是user,密码会在控制台输出
可以自己在yml配置文件中设置

spring:
  security: #httpBasic()认证用户名和密码
    user:
      name: wjc
      password: 123456

这样用户名就改成了wjc 密码也固定为123456了

首先我们要实现一个security的UserDetails

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private UserInfo userInfo;

    /**
     * 返回该账号下的所有权限信息
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        return authorities;
    }

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

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

    /**
     * 账号没有过期(默认返回true)
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账号没有被锁定(默认返回true)
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 密码没有过期(默认返回true)
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 账号可用(默认返回true)
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

我们先不考虑角色权限这块先就单纯实现登录块功能

这个userinfo是你自定义的用户实体类,我的是这样的

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("用户主题类")
public class UserInfo {

    @ApiModelProperty("用户id")
    private Integer id;

    @ApiModelProperty("用户名用来登录")
    private String userName;

    @ApiModelProperty("昵称")
    private String nickName;

    @ApiModelProperty("真实姓名")
    private String realName;

    @ApiModelProperty("手机号码")
    private String phone;

    @ApiModelProperty("密码")
    private String password;

    /**
    * 1:启用,2禁用
    */
    @ApiModelProperty("状态 1:启用 2:禁用")
    private Integer status;

    @ApiModelProperty("创建时间")
    private Date createTime;

    @ApiModelProperty("更新时间")
    private Date updateTime;
    }

这些注解是Lombok和swagger的无足轻重

我们还要实现security的UserDetailsService

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserInfoService userInfoService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
       //通过用户名查找用户
        UserInfo user = userInfoService.findByUserName(userName);
        if (user==null){
            //用户为空的话直接返回空值
            return null;
        }
        //将查找的用户封装到LoginUser类中
        return new LoginUser(user);
    }
}

UserInfoService就是user实体类的service层这里就是通过用户名从数据库里查询到用户的数据然后封装到我们实现的UserDetails接口LoginUser里去

做到这里其实我们已经可以成功完成登录功能了,但我们还无法生成token,所以我们需要一个TokenUtils
不理解jwt的可以去官网看看

@Component
@Lazy
@Data
public class TokenUtils implements Serializable {

    @Value("${token.header}")
    private String header;

    @Value("${token.secret}")
    private String secret;

    @Value("${token.expireTime}")
    private int expireTime;

    /**
     * 获取Token
     * @param claims
     * @return
     */
    public String createToken(Map<String,Object>claims){
        //设置token过期时间(毫秒数)
        long now = System.currentTimeMillis()+(expireTime * 60 * 1000);
        return Jwts.builder()
                //签发者
                .setIssuer("wjc")
                .addClaims(claims)
                //设置过期时间
                .setExpiration(new Date(now))
                .signWith(SignatureAlgorithm.HS256,secret).compact();
    }

    /**
     * 解析token
     * @param token
     * @return
     */
    public Claims parseToken(String token){
        JwtParser jwtParser = Jwts.parser().setSigningKey(secret);
        Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
        Claims body = claimsJws.getBody();
        return body;
    }


}

上面的三个属性是从yml配置文件动态注入的,方便以后token生成的更改,这个token工具类不是很完善,网上还有很多根据自己的需求选择即可

#token配置
token:
  header: Authorization #令牌自定义标识
  secret: wjc-key #令牌密钥
  expireTime: 30 #令牌有效期 默认30分钟


好了接下来就是重点的security配置类了
这里我们重点要写代码的就是重写两个处理器
登录成功处理器:successHandler
登录失败处理器:failureHandler

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解标注 哪些方法需要鉴权 @PreAuthorize("hasRole('admin')") @PreAuthorize("hasAuthority('sys:user:save')")
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    //实现了UserDetailsService的类
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private TokenUtils tokenUtils;


	//这里是定义了放行的路径
    private static final String[] URL_WHITELISTS ={
            "/login/**",
            "/swagger-ui.html",
            "/swagger-resources/**",
            "/webjars/**",
            "/v2/**",
            "/api/**"
    };



    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder(){
			//这里是可以更改密码加密的方式,security自带的BCryptPassword加密这个安全性更高
			也可以根据自己的需求更改
            @Override
            public String encode(CharSequence rawPassword) {
                return super.encode(rawPassword);
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {

                return super.matches(rawPassword, encodedPassword);
            }
        };
    }



        @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //springsecurity通过userDetailsService的loadUserByUsername方法
        //去数据库里查询用户并认证
        auth.userDetailsService(userDetailsService)
                //设置密码加密方式,默认为BCryptPasswordEncoder,也是springsecurity默认的密码加密方式
                //这个必须要
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable() //这里是解决跨域问题和禁用csrf
                .authorizeRequests()
                //白名单放行
                .antMatchers(URL_WHITELISTS).permitAll()
                //其他所有请求都要认证
                .anyRequest().authenticated()
                //连接
                .and()
                //关闭session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //登录
                .formLogin()
                //登录成功处理
                .successHandler(successHandler())
                //登录失败处理
                .failureHandler(failureHandler());

    }




    /**
     *登录成功处理器
     * @return
     */
    public AuthenticationSuccessHandler successHandler(){
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setCharacterEncoding("utf-8");
                httpServletResponse.setContentType("application/json;charset=utf-8");
                //取出此时登录的用户名
                String userName = authentication.getName();
                UserInfo userInfo = userInfoService.findByUserName(userName);
                Map<String,Object> claims = new HashMap<>();
                //我们将用户名存到token当中去
                claims.put("username",userInfo.getUserName() );
                //生成token
                String token = tokenUtils.createToken(claims);
                httpServletResponse.addHeader(LoginUtil.AUTH,token);
                PrintWriter writer = httpServletResponse.getWriter();
                //将token包装到同一的返回结果类返回
                writer.println(JsonResult.success(userInfo));
                //刷新确保成功响应
                writer.flush();
                writer.close();
            }
        };
    }

	 /**
     *登录失败处理器
     * @return
     */
    public AuthenticationFailureHandler failureHandler(){
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                httpServletResponse.setCharacterEncoding("utf-8");
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter writer = httpServletResponse.getWriter();
                writer.println(JsonResult.failure("登录失败"));
                writer.flush();
                writer.close();
            }
        };
    }

}

上面的JsonResult是同意封装的jsno返回类

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel(value = "通用公共返回类")
public class JsonResult <T> implements Serializable {

    public static final int CODE_SUCCESS = 200;
    public static final int CODE_FAILURED = 500;
    public static final String[] NOOP = new String[] {};

    @ApiModelProperty(value = "返回状态")
    private int code; // 处理状态:0: 成功
    @ApiModelProperty(value = "返回消息")
    private String message;
    private T data; // 返回数据

    /**
     * 有data的正常返回
     *
     * @param data data内容
     * @param <T> data类型
     */
    public static <T> JsonResult<T> success(T data) {
        return new JsonResult<>(CODE_SUCCESS,"success",data);
    }

    public static <T> JsonResult<T> success() {
        return new JsonResult<>(CODE_SUCCESS,"success",null);
    }
    public static <T> JsonResult<T> success(Integer code) {
        return new JsonResult<>(code,"success",null);
    }
    public static <T> JsonResult<T> success(Integer code,String message) {
        return new JsonResult<>(code,message,null);
    }
    public static <T>JsonResult<T> success(Integer code, String message,T data) {
        return new JsonResult<>(code,message,data);
    }
    public static <T> JsonResult<T> success(Integer code,T data) {
        return new JsonResult<>(code,"success",data);
    }

    public static JsonResult failure(String message) {
        return new JsonResult(CODE_FAILURED,message,null);
    }

    public static <T>  JsonResult<T>  failure(Integer code,String message,T data) {
        return new JsonResult(code,message,data);
    }
}

常量定义在这了

public class LoginUtil {
    //token的名称
   public static String AUTH = "Authorization";
    //token前缀
   public static String TOKEN_PREFIX = "Bearer ";
   
}

这个时候我们已经可以成功完成登录操作并下发token

接下来就要实现第二个功能解析token并且放行

@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter implements LoginUtil {

    @Autowired
    private TokenUtils tokenUtils;

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //配置的自定义标识 Authorization
        String token = request.getHeader(AUTH);
        //Bearer (有个空格)标识
        if (StrUtil.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) {
            //生成的token中带有Bearer 标识,去掉标识后就剩纯粹的token了。
            String substring = token.substring(TOKEN_PREFIX.length());
            try {
                //解析token拿到我们生成token的时候存进去的username
                Claims claims = tokenUtils.parseToken(substring);
                String userName = (String) claims.get("username");
                if (StrUtil.isNotBlank(userName)) {
                    //将查询到的用户信息取其账号(登录凭证)以及密码去生成一个Authentication对象
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, null,null);
                    //将Authentication对象放进springsecurity上下文中(进行认证操作)
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }catch (Exception e){
                //这里在解析错误token或token过期时会报异常
                log.error("解析token异常"+e.getMessage());
            }
        }
        //走下一条过滤器
        chain.doFilter(request,response);
    }
}

提一嘴这个**Bearer **(后面带空格的) 标识,就是前端在设置token要加这个前缀这个官网有提到,是规范,不过好像很多前端都不会带这个如果没有就把前面那个去前缀的去掉就可以了。这里try catch了一下是因为当token过期或者token错误时会有异常

然后我们要把这个过滤器加到security过滤链中去
还可以重写一下我们的认证处理器,对为加正确token的访问进行处理

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解标注 哪些方法需要鉴权 @PreAuthorize("hasRole('admin')") @PreAuthorize("hasAuthority('sys:user:save')")
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    //实现了UserDetailsService的类
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private TokenUtils tokenUtils;



    private static final String[] URL_WHITELISTS ={
            "/login/**",
            "/swagger-ui.html",
            "/swagger-resources/**",
            "/webjars/**",
            "/v2/**",
            "/api/**"
    };

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        return jwtAuthenticationFilter;
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder(){

            @Override
            public String encode(CharSequence rawPassword) {
                return super.encode(rawPassword);
            }

            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {

                return super.matches(rawPassword, encodedPassword);
            }
        };
    }



        @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        //springsecurity通过userDetailsService的loadUserByUsername方法
        //去数据库里查询用户并认证
        auth.userDetailsService(userDetailsService)
                //设置密码加密方式,默认为BCryptPasswordEncoder,也是springsecurity默认的密码加密方式
                //这个必须要
                .passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                //白名单放行
                .antMatchers(URL_WHITELISTS).permitAll()
                //其他所有请求都要认证
                .anyRequest().authenticated()
                //连接
                .and()
                //关闭session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                //登录
                .formLogin()
                //登录成功处理
                .successHandler(successHandler())
                //登录失败处理
                .failureHandler(failureHandler())

                .and()
                //设置访问被拒绝后的事件(用来处理权限不足时的返回)
                .exceptionHandling()
                //未登录访问资源
                .authenticationEntryPoint(AuthenticationEntryPoint())
                //将我们自定义的token过滤器按照一定顺序加入过滤器链
                .and()
                .addFilter(jwtAuthenticationFilter());

    }




    /**
     *登录成功处理器
     * @return
     */
    public AuthenticationSuccessHandler successHandler(){
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setCharacterEncoding("utf-8");
                httpServletResponse.setContentType("application/json;charset=utf-8");
                //取出此时登录的用户名
                String userName = authentication.getName();
                UserInfo userInfo = userInfoService.findByUserName(userName);
                Map<String,Object> claims = new HashMap<>();
                claims.put("username",userInfo.getUserName() );
                //生成token
                String token = tokenUtils.createToken(claims);
                httpServletResponse.addHeader(LoginUtil.AUTH,token);
                PrintWriter writer = httpServletResponse.getWriter();
                //将token包装到同一的返回结果类返回
                writer.println(JsonResult.success(userInfo));
                //刷新确保成功响应
                writer.flush();
                writer.close();
            }
        };
    }

    /**
     *登录失败处理器
     * @return
     */
    public AuthenticationFailureHandler failureHandler(){
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                httpServletResponse.setCharacterEncoding("utf-8");
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter writer = httpServletResponse.getWriter();
                writer.println(JsonResult.failure("登录失败"));
                writer.flush();
                writer.close();
            }
        };
    }

    /**
     *认证处理器
     * @return
     */
    public AuthenticationEntryPoint AuthenticationEntryPoint(){
        return new AuthenticationEntryPoint(){


            @Override
            public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
               //前后端分离项目 /login 可以是返回一个json字符串
                httpServletResponse.setCharacterEncoding("utf-8");
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter writer = httpServletResponse.getWriter();
                writer.println(JsonResult.failure("未登录!或登录失效请重新登录!"));
                writer.flush();
                writer.close();
            }
        };
    }

}

至此我们的两个功能都可以简单实现了

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot和Vue.js是两个非常流行的技术栈,可以非常好地实现前后端分离的开发模式。SecurityJWT是两个很好的工具,可以帮助我们实现安全的登录和授权机制。 以下是实现Spring Boot和Vue.js前后端分离的步骤: 1.创建Spring Boot工程 首先,我们需要创建一个Spring Boot工程,可以使用Spring Initializr来生成一个基本的Maven项目,添加所需的依赖项,包括Spring SecurityJWT。 2.配置Spring SecuritySpring Security中,我们需要定义一个安全配置类,该类将定义我们的安全策略和JWT的配置。在这里,我们可以使用注解来定义我们的安全策略,如@PreAuthorize和@Secured。 3.实现JWT JWT是一种基于令牌的身份验证机制,它使用JSON Web Token来传递安全信息。在我们的应用程序中,我们需要实现JWT的生成和验证机制,以便我们可以安全地登录和授权。 4.配置Vue.js 在Vue.js中,我们需要创建一个Vue.js项目,并使用Vue CLI来安装和配置我们的项目。我们需要使用Vue Router来定义我们的路由,并使用Axios来发送HTTP请求。 5.实现登录和授权 最后,我们需要实现登录和授权机制,以便用户可以安全地登录和访问我们的应用程序。在Vue.js中,我们可以使用Vue Router和Axios来发送HTTP请求,并在Spring Boot中使用JWT来验证用户身份。 总结 以上是实现Spring Boot和Vue.js前后端分离的步骤,我们可以使用SecurityJWT来实现安全的登录和授权机制。这种开发模式可以让我们更好地实现前后端分离,提高我们的开发效率和应用程序的安全性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值