spring security新人使用关键-简单地完成注册、登陆、JWT令牌访问

虽然出生科班,但由于我不是一个职业程序员,对很多前沿技术了解不够,今天我试图为自己的一个项目加上spring security框架,以下是一些需要注意的点

首先是引入包:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
        
        <!-- jjwt:JSON Web令牌工具 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

然后我们先做一个注册功能,注册功能全程用不到spring security,就是简单地从前端拿到表单,然后把密码加密一次,然后把用户名和密码塞到数据库里就行了,这里有一定基础的应该都会。
需要注意,在我们加密这个密码的时候,我们的加密方法应该和登陆时候将用到的加密方法一致,否则会对不上密码造成登陆失败

我这里是这样解决的,我使用了org.springframework.security.crypto.password.PasswordEncoder这个类,把它注册成一个Bean放在spring容器里。然后在注册的时候用上它:

	@Autowired
    PasswordEncoder passwordEncoder;

	然后在插入注册数据到数据库之前用这个东西加密它
	String password = passwordEncoder.encode(userForLogin.getPassword());
	这个encode用到的具体加密方法会在后面介绍

那么接下来是登陆的功能

首先是登陆的页面,这里我使用的是前后端分离,我的前端是一个react项目,前端会简单地传来一个表单,表单里包括username和password两个属性。
所以我用不到spring security自带的登陆页面,我们不希望自带的登陆页面出现,那么我们就要自己来控制非登陆情况下的访问控制
我们实现一个叫做AuthenticationEntryPoint的接口,用来控制这种情况:

@Component
public class MovieAuthenticationEntryPoint implements AuthenticationEntryPoint{
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=utf-8");
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("code", "-1");
                jsonObject.put("msg", "no permited");
                String json = objectMapper.writeValueAsString(jsonObject);
                PrintWriter out = response.getWriter();
                out.write(json);
                out.flush();
                out.close();
    }
    
}

上面的实现主要是在返回体response里面塞了一个json信息,这里想做什么都行,可以替代spring security原本不登陆就返回给你登陆页面这个行为。

接下来是登陆成功了干什么,我们需要实现一个接口AuthenticationSuccessHandler,在里面具体实现登陆成功后干什么


@Component
public class MovieAuthenticationSuccessHandler implements AuthenticationSuccessHandler{

    @Resource
    JwtUtils jwtUtils;

    @Autowired
    ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
                String username = ((UserDetails)authentication.getPrincipal()).getUsername();
                String token = jwtUtils.generateToken(username);
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=utf-8");
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("code", "0");
                jsonObject.put("msg", "login success");
                jsonObject.put("username",username);
                jsonObject.put("token", token);
                String json = objectMapper.writeValueAsString(jsonObject);
                PrintWriter out = response.getWriter();
                out.write(json);
                out.flush();
                out.close();
          } 
    
}

在这里,我们往返回体response里塞了一些状态码和用户信息,注意这里我们用JwtUtils来生成了一个令牌,也塞了进去,这个JwtUtils令牌我们会在后面介绍,它的主要作用就是让前端在登陆成功以后,每个请求都带上令牌来,就不用再重新登陆了,验证令牌就行了。

然后是登陆失败了干什么,一样的,我们实现一个接口AuthenticationFailureHandler,然后在里面具体地实现


@Component
public class MovieAuthenticationFailureHandler implements AuthenticationFailureHandler{

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType("application/json;charset=utf-8");
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("code", "-2");
                jsonObject.put("msg", "login failure");
                String json = objectMapper.writeValueAsString(jsonObject);
                PrintWriter out = response.getWriter();
                out.write(json);
                out.flush();
                out.close();
          }
    
}

这里我们也是往返回体塞了一些状态码和信息。

接下来是怎么验证用户名和密码对不对的问题,我们要创造一个Service,实现一个接口UserDetailsService

@Service
public class MovieUserDetailsService implements UserDetailsService{

    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名从数据库中查询用户信息
        Query query = new Query(Criteria.where("username").is(username));
        UserPojo user =  mongoTemplate.findOne(query,UserPojo.class);
        
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 返回一个UserDetails对象,包含用户名、密码和权限
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("all"));
    }
    
}

这里我用的数据库是mongodb,但用什么都一样。
这里面的关键是:1.要用username从数据库中把密码查出来。2.把查出来的用户名、密码、角色等塞进org.springframework.security.core.userdetails.User这个类里并返回,这里我没用到角色,所以设置了一个all,其实并没有用到。
这个对象返回之后,spring security会自动使用你返回的对象里的用户名密码和登陆的信息比对。

这四个东西做好后,我们要把他们使用起来,我们要用到一个配置类,我们的配置类需要继承WebSecurityConfigurerAdapter这个类


@Configurable
@EnableWebSecurity
public class MovieSecurityConfig extends WebSecurityConfigurerAdapter{
    
    @Autowired
    private MovieUserDetailsService movieUserDetailsService;

    @Autowired
    private MovieAuthenticationFailureHandler movieAuthenticationFailureHandler;
    @Autowired
    private MovieAuthenticationSuccessHandler movieAuthenticationSuccessHandler;

    @Autowired
    private MovieAuthenticationEntryPoint movieAuthenticationEntryPoint;

    @Resource
    JwtAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests().antMatchers("/regist").permitAll().antMatchers("/login/").permitAll()
            .anyRequest().authenticated().and().formLogin().loginProcessingUrl("/login")
            .usernameParameter("username").passwordParameter("password")
            .successHandler(movieAuthenticationSuccessHandler).failureHandler(movieAuthenticationFailureHandler)
            .and().exceptionHandling().authenticationEntryPoint(movieAuthenticationEntryPoint)
            .and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(movieUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Bean(name = "passwordEncoder")
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

先说configure,这里重要的是antMatchers( ) 这个方法,这个方法决定了你要匹配什么路径,然后在后面用permitAll或者其他方法来控制这个路径的访问,permitAll是所有人都可以访问的意思。这个方法主要是用来开后门的,比如注册、登陆,就不要求你已经认证。
anyRequest().authenticated() 这句的意思是所有其他没有被antMatchers( ) 特别标注的路径都要认证过才能访问。
formLogin().loginProcessingUrl(“/login”) .usernameParameter(“username”).passwordParameter(“password”) 这句的意思是使用表单登陆,登陆路径是/login,表单中用户名的变量叫username,密码的变量叫password。这个方法主要作用就是明确登陆要从哪里登陆,约定好用户名和密码的变量名,以便spring security后续核对密码使用。
.successHandler(movieAuthenticationSuccessHandler).failureHandler(movieAuthenticationFailureHandler).and().exceptionHandling().authenticationEntryPoint(movieAuthenticationEntryPoint) 这一句的意思是约定好登陆成功了干什么、登陆失败了干什么,我们把前面做好的那两个类放进去,然后exceptionHandling是说非登陆的访问怎么办,也是给他加上前面做好的第一个类。
这样一来,刚刚做好的那几个类就全部用上了。
.and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); 这一句,是加上了一个JWT令牌的过滤器,这个过滤器会在检查登陆状态之前执行,也是需要我们自己做的,我们会在后面介绍。

auth.userDetailsService(movieUserDetailsService).passwordEncoder(new BCryptPasswordEncoder()); 接下来是这一句,这一句的用到了我们前面做好的第四个类,我们从数据库中将用户名密码拿出来,spring security通过这个方法和前端传来的用户名和密码比对。这里我们加入了密码加密方法,用的是BCryptPasswordEncoder,这里你不论用哪种加密方法,要和注册时用到的一致。
为了方便一致地使用这个加密方式,我们在最后创造了一个Bean,就是最后一段的passwordEncoder ,用的也是BCryptPasswordEncoder加密,在注册的时候,我们就用到过这个Bean。

做到这里,我们已经可以完成注册和登陆了,但是这样每次访问资源都要重新登陆,非常不方便,于是接下来我们要用到令牌,只要前端带着令牌来,我们检查令牌有效,就算他登陆过了。
首先我们做一个工具类

@Component
public class JwtUtils {

    @Value("secretkey")
    private String SECRET_KEY;

    // 定义一个令牌有效期,单位为毫秒
    private static final long EXPIRATION_TIME = 1000 * 60 * 60 * 24;

    // 生成令牌的方法,接收一个用户名作为参数
    public  String generateToken(String username) {
        // 使用JwtBuilder创建一个令牌
        JwtBuilder builder = Jwts.builder()
                .setSubject(username) // 设置主题
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 设置过期时间
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY); // 设置签名算法和密钥
        // 返回生成的令牌字符串
        return builder.compact();
    }

    // 解析令牌的方法,接收一个令牌字符串作为参数,返回一个用户名
    public  String parseToken(String token) {
        // 使用JwtParser解析令牌
        JwtParser parser = Jwts.parser()
                .setSigningKey(SECRET_KEY); // 设置密钥
        // 获取令牌中的主体
        String username = parser.parseClaimsJws(token)
                .getBody()
                .getSubject();
        // 返回主体
        return username;
    }

    // 验证令牌的方法,接收一个令牌字符串作为参数,返回一个布尔值,表示令牌是否有效
    public  boolean validateToken(String token) {
        try {
            // 使用JwtParser解析和验证令牌,如果成功则返回true,如果失败则抛出异常
            Jwts.parser()
                    .setSigningKey(SECRET_KEY)
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            // 捕获异常并返回false
            return false;
        }
    }
}

这里注释比较详细,我就不赘述了,SECRET_KEY这个密钥我是直接设置在application.properties里面的,方便改动。

我们需要将请求在到达检查用户名密码这一步之前拦截,然后验证令牌,如果令牌有效,就直接将本次请求设置成认证过的状态。
于是我们先做一个拦截器,要继承 OncePerRequestFilter这个类

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter{

    @Resource
    JwtUtils jwtUtils;

    @Autowired
    ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
                String token = request.getHeader("token");
                Boolean valid = jwtUtils.validateToken(token);
                if(valid){
                    String username = jwtUtils.parseToken(token);
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, null);
                    
                    // 将authentication对象设置到SecurityContextHolder中,表示用户已经通过认证,
                    //并可以访问资源了
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
                filterChain.doFilter(request, response);
    }
    
}

可以看到,我们先从request里面拿到header里面包含的token,然后用刚刚的工具进行验证,如果验证结果为真,就新建一个UsernamePasswordAuthenticationToken,然后把它塞进SecurityContextHolder里,那么本次访问就算是认证过的了,后面的登陆、核对密码这些事情就不会再拦截。
然后我们回到设置类,上文提到过,设置类里有一句 .and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); ,我们将刚刚的拦截器放在里面,这里加入的拦截器会在验证用户名密码之前执行,所以我们的自制拦截器就会首先被执行,如果令牌对了,就放行。

如此一来,我们就简单地完成了一个spring security框架的加入,对已存在的项目没有任何侵入,实现了注册、登陆、令牌访问三个功能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值