Angular 6集成Spring Boot 2,Spring Security,JWT和CORS系列:四、heroapi项目中用Spring Security实现用户和密码的认证及返回JWT令牌

一、基本认证实现

     使用Spring Security提供的UsernamePasswordAuthentication进行认证,在我们前面的代码基础之上,仅需要进行两个步骤即可:

1、实现Spring Security的UserDetailsService

/**
 * 
 */
package com.jh.heroes.api.service;

import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.jh.heroes.api.domain.SysUser;
import com.jh.heroes.api.repository.SysUserRepository;

/**
 * 对UserDetailsService的实现,为Spring Security返回UserDetails.
 * @author liangxh
 *
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	
	@Autowired
	private SysUserRepository sysUserRepository;

	/* (non-Javadoc)
	 * @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
	 */
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		SysUser queryUser = new SysUser();
		queryUser.setUsername(username);
		Example<SysUser> example = Example.of(queryUser);
		Optional<SysUser> sysUserOptional=sysUserRepository.findOne(example);
		if (sysUserOptional.isPresent()) {
			SysUser sysUser=sysUserOptional.get();
			return new User(sysUser.getUsername(),sysUser.getPassword(),
					sysUser.getRoles().stream().map(role->new SimpleGrantedAuthority(role.getName().name())).collect(Collectors.toList())
					);
		} else {
			throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
		}
	}
}

实现UserDetailsService接口,返回UserDetails实现User。

2、修改配置文件WebSecurityConfig

package com.jh.heroes.api.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	
	/**
	 * 装载BCrypt密码编码器
	 * 
	 * @return
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Autowired
    private UserDetailsService userDetailsService;

	
	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//关闭csrf,所有请求允许访问
		http.cors()
		.and()
		.csrf().disable()
		.formLogin()
		.and()
		.authorizeRequests()
		.antMatchers("/login").permitAll()
		.anyRequest()
		.authenticated();
	}

}

注:A、注入UserDetailsService,配置AuthenticationManager,告之我们使用的UserDetailsService和PasswordEncoder。

       B、HttpSecurity中,formLogin登录,除login外所有访问地址都需要认证。

3、浏览器测试

启动系统,在浏览器中输入http://localhost:8001/user,浏览器页面跳转到http://localhost:8001/login,如下图

 在User中录入admin,在Password中录入123456,在点击Login按钮,浏览器又跳转到http://localhost:8001/user,如下图显示

一切ok,非常完美。

4、postman测试

在postman中,地址栏输入http://localhost:8001/login,HttpMethod中选择post,body中选择form-data,然后按下图输入,再点击“send”按钮,获得如下图所示

这个显示没有根路径。

又按照下图测试,

heroapi不接受json的参数。

这两种情况,对于前后端分离以及移动端提供接口,都没有办法使用。伟大的Spring Security,很早就考虑这个问题了:

对于第一种情况,扩展认证成功和失败的处理器即可;第二种情况,新加一个认证过滤器即可。

二、扩展认证成功和失败处理器

1、扩展认证成功处理器

/**
 * 
 */
package com.jh.heroes.api.web.authentication;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 身份认证成功处理器
 * @author liangxh
 *
 */
@Component("heroApiAuthenticationSuccessHandler")
public class HeroApiAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
	
	private Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	private ObjectMapper objectMapper;


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.web.authentication.
	 * AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
	 * HttpServletRequest, javax.servlet.http.HttpServletResponse,
	 * org.springframework.security.core.Authentication)
	 */
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {

		logger.info("登录成功");

		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(authentication));

	}

}

根据我们实际要求,通过HttpServletReponse重写返回的数据。

2、扩展认证失败处理器

/**
 * 
 */
package com.jh.heroes.api.web.authentication;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.exception.ErrorMessage;

/**
 * 身份认证失败处理器
 * 
 * @author liangxh
 *
 */
@Component("heroApiAuthenctiationFailureHandler")
public class HeroApiAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
	private Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	private ObjectMapper objectMapper;

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.web.authentication.AuthenticationFailureHandler#
	 * onAuthenticationFailure(javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse,
	 * org.springframework.security.core.AuthenticationException)
	 */
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {

		logger.info("登录失败:{}");

		response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(new ErrorMessage("-1", exception.getMessage())));

	}
}

3、注册新的成功和失败处理器

package com.jh.heroes.api.web.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	
	/**
	 * 装载BCrypt密码编码器
	 * 
	 * @return
	 */
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Autowired
    private UserDetailsService userDetailsService;
	
	@Autowired
    private AuthenticationSuccessHandler heroApiAuthenticationSuccessHandler;
    
    @Autowired
    private AuthenticationFailureHandler heroApiAuthenctiationFailureHandler;
	
	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//关闭csrf,所有请求允许访问
		http.cors()
		.and()
		.csrf().disable()
		.formLogin()
		.successHandler(heroApiAuthenticationSuccessHandler)
		.failureHandler(heroApiAuthenctiationFailureHandler)
		.and()
		.authorizeRequests()
		.antMatchers("/authentication/form").permitAll()
		.antMatchers("/login").permitAll()
		.anyRequest()
		.authenticated();
	}
}

依赖注入成功和失败处理器,然后在HttpSecurity中设置新的成功和失败处理器。

4、浏览器测试

在浏览器中录入http://localhost:8001/user,浏览器跳转到http://localhost:8001/login,录入账户和密码之后,返回json格式的Authentication内容,再开新标签页录入http://localhost:8001/user可以查看到所有用户的json信息。

5、postman测试

  无论正确还是错误输入用户和密码信息,都会获得我们需要的内容。同时,如认证成功,可以访问其它的接口;若认证不成功,访问其它接口,它返回Spring Security默认的登录页面。

三、Json参数登录过滤器实现

研究UsernamePasswordAuthenticationFilter,我们可以发现只要修改username、password取值的方法,以及过滤器拦截的地址,就可以新的认证方式。

1、新建JsonUsernamePasswordAuthenticationFilter类。除类名称外,完全拷贝UsernamePasswordAuthenticationFilter的代码,然后稍作修改即可。

A、修改拦截地址为"/jsonlogin"。修改JsonUsernamePasswordAuthenticationFilter类的构造方法

public JsonUsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/jsonlogin", "POST"));
	}

B、修改username、password的取值办法。这两个值从HttpServletRequest的InputStream获取,然后再JSON解析;修改attemptAuthentication方法为

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		String username;
		String password;
		try {
			ServletInputStream ris = request.getInputStream();
			StringBuilder content = new StringBuilder();
			byte[] b = new byte[1024];
			int lens = -1;
			while ((lens = ris.read(b)) > 0) {
				content.append(new String(b, 0, lens));
			}
			String strcont = content.toString();// 内容	
			ObjectMapper mapper = new ObjectMapper(); //转换器  
			Map<?, ?> map=mapper.readValue(strcont, Map.class);
			
			username = map.get(usernameParameter).toString();
			password = map.get(passwordParameter).toString();
		} catch (Exception e) {
			throw new AuthenticationServiceException("参数不存在");
		}

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		
		return this.getAuthenticationManager().authenticate(authRequest);
	}

2、修改WebSecurityConfig配置文件,加入json登录的过滤器

A、修改AuthenticationManager的注入,解决http.getSharedObject(AuthenticationManager.class)无法获取AuthenticationManager实例的问题

@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Autowired
    protected AuthenticationManager authenticationManager;

B、修改configure(HttpSecurity http),配置如下

@Override
	protected void configure(HttpSecurity http) throws Exception {
		JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter=new JsonUsernamePasswordAuthenticationFilter();
		jsonUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager);
		jsonUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(heroApiAuthenticationSuccessHandler);
		jsonUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(heroApiAuthenctiationFailureHandler);

		http
		.addFilterAfter(jsonUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
		.cors()
		.and()
		.csrf().disable()
		.formLogin()
		.successHandler(heroApiAuthenticationSuccessHandler)
		.failureHandler(heroApiAuthenctiationFailureHandler)
		.and()
		.authorizeRequests()
		.antMatchers("/jsonlogin").permitAll()
		.antMatchers("/login").permitAll()
		.anyRequest()
		.authenticated();
	}

首先new 一个JsonUsernamePasswordAuthenticationFilter,设置它的AuthenticationManager以及认证成功、失败处理器;再把它注册到Spring Security过滤器链之上,并且在UsernamePasswordAuthenticationFilter之后生效的过滤器;最后再配置允许"/jsonlogin"访问.

3、postman测试

在postman中,在http://localhost:8001/jsonlogin 无能输入的json参数正确与否,都可以获得我们需要的结果。

这样,我们系统就同时支持两种形式的登录。

四、认证成功返回jwt

1、添加jwt的依赖包,站在牛人的基础上,简化jwt的生成和验证.在maven创库中搜索jjwt,使用io.jsonwebtoken的jwt。

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

2、添加jwt工具类

package com.jh.heroes.api.util;

import java.security.Key;
import java.util.Date;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

import org.springframework.security.core.userdetails.UserDetails;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class JwtHelper {
	
	private static final String base64Security = "mySecretofheroesapi";

	/**
	 * 解密jwt.
	 * @param jsonWebToken 令牌
	 * @param base64Security base64的秘钥
	 * @return
	 */
	public static Claims parseJWT(String jsonWebToken){
		try
		{
			Claims claims = Jwts.parser()
					   .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
					   .parseClaimsJws(jsonWebToken).getBody();
			return claims;
		}
		catch(Exception ex)
		{
			return null;
		}
	}
	
	/**
	 * 生成令牌.
	 * @param user UserDetails
	 * @return 令牌
	 */
	public static String createJWT(UserDetails user) 
	{
		SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
		//生成签名密钥
		byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security);
		Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
		 
		  //添加构成JWT的参数
		JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
										.claim("role", user.getAuthorities())
				                        .claim("username", user.getUsername())
				                        .setIssuer("lxhjh")
				                        .setSubject(user.getUsername())
				                        .setIssuedAt(new Date())
		                                .signWith(signatureAlgorithm, signingKey)
		                                .setNotBefore(new Date())
		                                .setExpiration(new Date(System.currentTimeMillis() + 7*24*3600 * 1000));		 
		 //生成JWT
		return builder.compact();
	} 
	
}

3、增加统一的接口调用返回对象

package com.jh.heroes.api.web.authentication;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseMsg implements Serializable{

	/**
	 * 序列化.
	 */
	private static final long serialVersionUID = -8739271187671909578L;
	
	public static final Integer STATUS_SUCCESS = 0;
	public static final Integer STATUS_FAILED = -1;
	
	/**
	 * 状态编码.
	 */
	private Integer status=0;
	/**返回消息.*/
	private String msg="";
	/**返回数据.*/
	private Object data;
	
	
	/**
	 * 失败.
	 * @param msg 失败原因
	 */
	public void failed(String msg)
	{
		this.setStatus(STATUS_FAILED);
		this.setMsg(msg);
	}
	
}

4、修改认证成功处理器

/**
 * 
 */
package com.jh.heroes.api.web.authentication;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jh.heroes.api.util.JwtHelper;

/**
 * 身份认证成功处理器
 * @author liangxh
 *
 */
@Component("heroApiAuthenticationSuccessHandler")
public class HeroApiAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
	
	private Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	private ObjectMapper objectMapper;
	
	@Autowired
	private UserDetailsService userDetailsService;


	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.security.web.authentication.
	 * AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.
	 * HttpServletRequest, javax.servlet.http.HttpServletResponse,
	 * org.springframework.security.core.Authentication)
	 */
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		logger.info("登录成功");
		UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getName());
		String token= JwtHelper.createJWT(userDetails);
		ResponseMsg msg=new ResponseMsg();
		msg.setData(token);
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(msg));

	}

}

调用jwt工具类生成令牌,让后用统一返回类包装令牌放回。

5、修改认证错误处理器

/**
 * 
 */
package com.jh.heroes.api.web.authentication;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 身份认证失败处理器
 * 
 * @author liangxh
 *
 */
@Component("heroApiAuthenctiationFailureHandler")
public class HeroApiAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
	private Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	private ObjectMapper objectMapper;

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.security.web.authentication.AuthenticationFailureHandler#
	 * onAuthenticationFailure(javax.servlet.http.HttpServletRequest,
	 * javax.servlet.http.HttpServletResponse,
	 * org.springframework.security.core.AuthenticationException)
	 */
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {

		logger.info("登录失败:{}");
		
		ResponseMsg msg=new ResponseMsg();
		msg.failed(exception.getMessage());

		response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
		response.setContentType("application/json;charset=UTF-8");
		response.getWriter().write(objectMapper.writeValueAsString(msg));

	}
}

这个任然用了返回状态码。

6、postman测试

成功时,返回

失败时,返回

或者

五、小结

本节利用Spring Security的过滤器链原理,扩展JSON参数的认证;同时使用它的认证成功、认证失败处理器,对认证的返回进行统一处理,从而避免参考资料中的用一个controller来处理登录。

参考:

1、Spring Security无法注入authenticationManager:No qualifying bean of type AuthenticationManager found for

2、Spring Boot实战之Filter实现使用JWT进行接口认证

3、JWT的Java使用 (JJWT)

猜你喜欢

转载自blog.csdn.net/lxhjh/article/details/82259627