一、基本认证实现
使用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