这篇博文讲述的是不集成oath,通过自己编写jwt 的 token 生成器 实现 spring security 的 登录权限token认证的实现方法。
目录结构如下:
pom文件 加入 springsecurity 和 JWT的引用包
<!-- spring security -->
<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.1</version>
</dependency>
建立spring security 的 SecurityConfiguration 类
import com.canic.dkd.authorization.security.Exception.MyAccessDeniedHandler;
import com.canic.dkd.authorization.security.Exception.MyAuthenticationEntryPoint;
import com.canic.dkd.authorization.security.verifyCode.CustomAuthenticationProvider;
import com.canic.dkd.authorization.security.verifyCode.VerifyServlet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationDetailsSource;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.http.HttpServletRequest;
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired private SysUserDetailsService sysUserDetailsService;
@Autowired private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
/* 自己编写的认证逻辑替换系统自带认证 */
@Autowired private CustomAuthenticationProvider customAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/* 注册自己编写的认证逻辑 */
auth.authenticationProvider(customAuthenticationProvider);
auth.userDetailsService(sysUserDetailsService).passwordEncoder(new PasswordEncoder() {
@Override public String encode(CharSequence charSequence) { return charSequence.toString(); }
@Override public boolean matches(CharSequence charSequence, String s) { return s.equals(charSequence.toString()); }
});
}
@Override
public void configure(WebSecurity web) throws Exception {
/* 设置拦截忽略文件夹,可以对静态资源放行 */
web.ignoring().antMatchers("/css/**", "/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/auth/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
//.and()
//.logout()
//.logoutUrl("/auth/logout")
//.logoutSuccessUrl("/login.html")
/* 验证码 */
.authenticationDetailsSource(authenticationDetailsSource)
.and()
.authorizeRequests()
.antMatchers("/login.html", "/getVerifyCode","/favicon.ico").permitAll()
.anyRequest()
/* 认证+验权处理 */
.access("@rbacService.hasPermission(request, authentication)")
//.and()
/* 禁用session 不可放开 */
//.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable();
/* 注册自定义异常 */
http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint).accessDeniedHandler(myAccessDeniedHandler);
}
/**
* 密码加密
*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入验证码servlet
*/
@Bean
public ServletRegistrationBean indexServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(new VerifyServlet());
registration.addUrlMappings("/getVerifyCode");
return registration;
}
}
这个是总体配置springsecurity的配置类
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/auth/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
这几个配置是与登录相关的配置,login.html 是登录的页面, /auth/login是登录提交地址,这个地址是不用自己实现的,只是告诉spring security 对这个地址发起POST请求,就是要登录系统了
.authenticationDetailsSource(authenticationDetailsSource)
这句话是注册验证码的,如果不加验证码服务不用写这个
.access("@rbacService.hasPermission(request, authentication)")
这句话是很重要的一句话,所有的请求都会进入这个sevice,在这里判断要不要让请求通过。
===================================这里是插叙的分界线=================================
类中注入和引用的文件的说明如下
1、authenticationSuccessHandler
2、authenticationFailureHandler
1、2、这两个类是用于认证成功后的返回处理类,认证成功进入authenticationSuccessHandler,认证失败进入authenticationFailureHandler,可以在类中编写后续的业务处理逻辑,我这里只把提示信息反馈到前台;
3、myAuthenticationEntryPoint
4、myAccessDeniedHandler
3、4、这两个是自定义的认证失败,鉴权失败的错误提示,当系统抛出这两种失败异常的时候,就会自动进入咱们自定义的失败处理逻辑上,这里可以写错误提示返回到前台的json,不再提示系统原来不友好的英文提示。如果鉴权失败还可以分析失败原因,如果是因为token过期导致,可以刷新token后重新发起请求,同时把新token传递到前台。这里就不做这方面的功能了,只是把错误提示优化了一下。
5、sysUserDetailsService
spring security提供接口的实现方法,通过用户name查询出用户相应信息,返回要求的 UserDetails 对象,提供给springsecurity做验证时调用
6、authenticationDetailsSource
验证 验证码需要注册的类
7、indexServletRegistration
验证码是用servlet实现的,把这个servlet注册到springboot系统中
8、passwordEncoder()
密码加密,验证时候会用到,验证的是加密后的密码
9、customAuthenticationProvider
加入了验证码的逻辑,springsecurity提供的验证就不能用了,所以要自定义验证逻辑
===================================这里是插叙结束的分界线=================================
下边分别说一下每个类的写法和要实现的功能
SysUserDetailsService 负责提供用户信息
import com.canic.dkd.authorization.staff.entity.SysRole;
import com.canic.dkd.authorization.staff.entity.SysUser;
import com.canic.dkd.authorization.staff.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class SysUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.getUserByName(username);
if (user == null){
throw new UsernameNotFoundException("用户不存在!");
}
log.info("用户存在,用户:"+username);
List<SysRole> roleList = sysUserService.getRoleListByUserId(user.getId());
List<SimpleGrantedAuthority> userGrantedAuthorities = createAuthorities(roleList);
//返回数据对象(手动已加密)
return new User(user.getName(), passwordEncoder.encode(user.getPassword()), userGrantedAuthorities);
}
/**
* @param roleList
*/
private List<SimpleGrantedAuthority> createAuthorities(List<SysRole> roleList){
List<SimpleGrantedAuthority> userGrantedAuthorities = new ArrayList<SimpleGrantedAuthority>();
for (SysRole role : roleList) {
userGrantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
log.info("role:"+role.toString());
}
return userGrantedAuthorities;
}
}
JwtTokenUtil 创建token 这里只用了创建token的功能,其余功能要继续完善
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* jwt
*/
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 密钥
*/
private static final String secret = "qwe!@#$";
/**
* 从数据声明生成令牌
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + 2592000L * 1000);
return Jwts.builder()
.setClaims(claims)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从令牌中获取数据声明
* @param token 令牌
* @return 数据声明
*/
private static Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
}
return claims;
}
/**
* 生成令牌
* @param username 用户
* @return 令牌
*/
public static String generateToken(String username) {
Map<String, Object> claims = new HashMap<>(2);
claims.put("sub", username);
claims.put("created", new Date());
String token = generateToken(claims);
System.out.println("token is " + token);
return token;
}
/**
* 从令牌中获取用户名
* @param token 令牌
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String username = null;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
}
return username;
}
/**
* 判断令牌是否过期
*
* @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;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public static String refreshToken(String token) {
String refreshedToken = null;
try {
Claims claims = getClaimsFromToken(token);
claims.put("created", new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
}
return refreshedToken;
}
/**
* 验证令牌
* @param token 令牌
* @param userDetails 用户
* @return 是否有效
*/
public static Boolean validateToken(String token, UserDetails userDetails) {
//JwtUser user = (JwtUser) userDetails;
//String username = getUsernameFromToken(token);
//return (username.equals(user.getUsername()) && !isTokenExpired(token));
return false;
}
}
RbacAuthorityService 验证逻辑处理 验证的逻辑是:
如果请求头中携带了token,那把token中的用户信息解出来,验证该用户是否存在,如果存在,但是当前缓存中不存在该用户的认证,就通过手动的方式把用户的认证信息放到缓存中。并且继续验证这次请求的权限是不是该用户具备的权限,如果该用户有权限,就继续下一个过滤器,也就是放过该次请求。
如果用户没有携带token,当前缓存中也没有用户的认证信息,那请求就被拦截,并抛出认证异常;
如果用户通过了认证,但是不具备访问这次请求的权限,抛出无权限异常,请求被拦截;
import com.canic.dkd.authorization.staff.entity.SysRole;
import com.canic.dkd.authorization.staff.entity.SysUser;
import com.canic.dkd.authorization.staff.service.SysPermissionService;
import com.canic.dkd.authorization.staff.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* RBAC 所有的请求都会到这里做权限校验
* @author YUY
*/
@Slf4j
@Component("rbacService")
public class RbacAuthorityService {
@Bean
private AntPathMatcher antPathMatcher() {
return new AntPathMatcher();
}
@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserDetailsService sysUserDetailsService;
@Autowired
private SysPermissionService sysPermissionService;
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
log.info("current request is:" + request.getRequestURI());
List<SimpleGrantedAuthority> grantedAuthorities = (List<SimpleGrantedAuthority>)authentication.getAuthorities();
String token = request.getHeader("token");
/**
* 未登录 --> token验证通过:重构系统用户authentication,继续验权限 --> token验证不通过:拦截
*/
if (grantedAuthorities != null) {
SimpleGrantedAuthority roleName = grantedAuthorities.get(0);
if(roleName.getAuthority().equals("ROLE_ANONYMOUS")){
// 未登录&未提供token,直接拦截
if(token == null){
return false;
}
// 未登录&提供token但token未通过验证
String userName = JwtTokenUtil.getUsernameFromToken(token);
SysUser user = sysUserService.getUserByName(userName);
if (user == null){
return false;
}else{
Authentication refreshAuthentication = refreshAuthentication(user, request);
// 更新 authentication
grantedAuthorities = (List<SimpleGrantedAuthority>)refreshAuthentication.getAuthorities();
}
}
}
// 该URL所需要的角色
List<SysRole> urlRoleList = sysPermissionService.getRoleListByPermissionUrl(request.getRequestURI());
// 登录人拥有的角色
List<String> userRoleList = grantedAuthorities.stream().map(authory -> authory.getAuthority()).collect(Collectors.toList());
String userRoleStr= String.join(",", (String[])userRoleList.toArray(new String[userRoleList.size()]));
// 该用户的角色与该URL所需要的角色有重合
for(SysRole role : urlRoleList) {
if(userRoleStr.contains(role.getName())){
return true;
}
}
return false;
}
/**
* 重建该用户当前的权限
* @param user
*/
private Authentication refreshAuthentication(SysUser user, HttpServletRequest request){
UserDetails userDetails = sysUserDetailsService.loadUserByUsername(user.getName());
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
}
AuthenticationSuccessHandler 成功后处理
import com.canic.dkd.base.entity.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component("authenticationSuccessHandler")
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
logger.info("登录成功");
response.setContentType("application/json;charset=UTF-8");
String token = JwtTokenUtil.generateToken(authentication.getName());
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("token",token);
tokenMap.put("authentication",authentication);
response.getWriter().write(objectMapper.writeValueAsString(Result.successResult(tokenMap)));
}
}
AuthenticationFailureHandler 失败后处理
import com.canic.dkd.base.entity.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Component("authenticationFailureHandler")
public class AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException {
log.info("登录失败");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(Result.failureResult(e.getMessage())));
}
}
到上述都是有关验证的部分
如果是有验证码的操作,就要用到下边验证码的验证了 验证码生成类 VerifyServlet
import javax.imageio.ImageIO;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* 验证码生成类
* @author ZC
* @date 2019/5/6
*/
public class VerifyServlet extends HttpServlet {
private static final long serialVersionUID = -5051097528828603895L;
/**
* 验证码图片的宽度。
*/
private int width = 100;
/**
* 验证码图片的高度。
*/
private int height = 30;
/**
* 验证码字符个数
*/
private int codeCount = 4;
/**
* 字体高度
*/
private int fontHeight;
/**
* 干扰线数量
*/
private int interLine = 16;
/**
* 第一个字符的x轴值,因为后面的字符坐标依次递增,所以它们的x轴值是codeX的倍数
*/
private int codeX;
/**
* codeY ,验证字符的y轴值,因为并行所以值一样
*/
private int codeY;
/**
* codeSequence 表示字符允许出现的序列值
*/
char[] codeSequence = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
/**
* 初始化验证图片属性
*/
@Override
public void init() throws ServletException {
// 从web.xml中获取初始信息
// 宽度
String strWidth = this.getInitParameter("width");
// 高度
String strHeight = this.getInitParameter("height");
// 字符个数
String strCodeCount = this.getInitParameter("codeCount");
// 将配置的信息转换成数值
try {
if (strWidth != null && strWidth.length() != 0) {
width = Integer.parseInt(strWidth);
}
if (strHeight != null && strHeight.length() != 0) {
height = Integer.parseInt(strHeight);
}
if (strCodeCount != null && strCodeCount.length() != 0) {
codeCount = Integer.parseInt(strCodeCount);
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
//width-4 除去左右多余的位置,使验证码更加集中显示,减得越多越集中。
//codeCount+1 //等比分配显示的宽度,包括左右两边的空格
codeX = (width - 4) / (codeCount + 1);
//height - 10 集中显示验证码
fontHeight = height - 10;
codeY = height - 7;
}
/**
* @param request
* @param response
* @throws ServletException
* @throws java.io.IOException
*/
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {
// 定义图像buffer
BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D gd = buffImg.createGraphics();
// 创建一个随机数生成器类
Random random = new Random();
// 将图像填充为白色
gd.setColor(Color.WHITE);
gd.fillRect(0, 0, width, height);
// 创建字体,字体的大小应该根据图片的高度来定。
Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);
// 设置字体。
gd.setFont(font);
// 画边框。
gd.setColor(Color.BLACK);
gd.drawRect(0, 0, width - 1, height - 1);
// 随机产生16条干扰线,使图象中的认证码不易被其它程序探测到。
gd.setColor(Color.lightGray);
for (int i = 0; i < interLine; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
gd.drawLine(x, y, x + xl, y + yl);
}
// randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
StringBuffer randomCode = new StringBuffer();
int red = 0, green = 0, blue = 0;
// 随机产生codeCount数字的验证码。
for (int i = 0; i < codeCount; i++) {
// 得到随机产生的验证码数字。
String strRand = String.valueOf(codeSequence[random.nextInt(36)]);
// 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
red = random.nextInt(255);
green = random.nextInt(255);
blue = random.nextInt(255);
// 用随机产生的颜色将验证码绘制到图像中。
gd.setColor(new Color(red, green, blue));
gd.drawString(strRand, (i + 1) * codeX, codeY);
// 将产生的四个随机数组合在一起。
randomCode.append(strRand);
}
// 将四位数字的验证码保存到Session中。
HttpSession session = request.getSession();
session.setAttribute("validateCode", randomCode.toString());
// 禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
// 将图像输出到Servlet输出流中。
ServletOutputStream sos = response.getOutputStream();
ImageIO.write(buffImg, "jpeg", sos);
sos.close();
}
}
CustomWebAuthenticationDetails
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.http.HttpServletRequest;
/**
* 获取用户登录时携带的额外信息
* @author ZC
* @since 2019/5/6
*/
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 1L;
private final String verifyCode;
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
/* verifyCode为页面中验证码的name */
verifyCode = request.getParameter("verifyCode");
}
public String getVerifyCode() {
return this.verifyCode;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append("; VerifyCode: ").append(this.getVerifyCode());
return sb.toString();
}
}
CustomAuthenticationDetailsSource
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 该接口用于在Spring Security登录过程中对用户的登录信息的详细信息进行填充
* @author ZC
* @since 2019/5/6
*/
@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new CustomWebAuthenticationDetails(request);
}
}
CustomAuthenticationProvider 重写认证逻辑
import com.canic.dkd.authorization.security.Exception.MyAuthenticationException;
import com.canic.dkd.authorization.security.SysUserDetailsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义验证用户名密码验证码逻辑
* @author ZC
* @since 2019/5/6
*/
@Slf4j
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired private SysUserDetailsService sysUserDetailsService;
@Autowired private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
/* 获取用户输入的用户名和密码 */
String inputName = authentication.getName();
String inputPassword = authentication.getCredentials().toString();
CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
String verifyCode = details.getVerifyCode();
if (!validateVerify(verifyCode)) {
throw new MyAuthenticationException("验证码输入错误");
}
/* userDetails为数据库中查询到的用户信息 */
UserDetails userDetails = sysUserDetailsService.loadUserByUsername(inputName);
String password = userDetails.getPassword();
/* 密码加密后 */
String ecodePassword = passwordEncoder.encode(inputPassword);
/* 校验密码是否一致 */
if (passwordEncoder.matches(password,ecodePassword)) {
throw new MyAuthenticationException("密码错误");
}
return new UsernamePasswordAuthenticationToken(inputName, ecodePassword, userDetails.getAuthorities());
}
private boolean validateVerify(String inputVerify) {
/* 获取当前线程绑定的request对象 */
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
/* 这个validateCode是在servlet中存入session的名字 */
String validateCode = ((String) request.getSession().getAttribute("validateCode")).toLowerCase();
inputVerify = inputVerify.toLowerCase();
log.info("验证码:" + validateCode + "用户输入:" + inputVerify);
return validateCode.equals(inputVerify);
}
@Override
public boolean supports(Class<?> authentication) {
/* 和UsernamePasswordAuthenticationToken比较 */
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
以上是增加了验证码后的完整认证方法
下方是优化了自定义异常处理的方式,登录认证有关的异常是 认证失败异常,与鉴权有关的异常是无权限异常,分别做如下处理
import org.springframework.security.access.AccessDeniedException;
/**
* 自定义授权异常
*/
public class MyAccessDeniedException extends AccessDeniedException {
public MyAccessDeniedException(String msg) {
super(msg);
}
}
import org.springframework.security.core.AuthenticationException;
/**
* 自定义认证异常
*/
public class MyAuthenticationException extends AuthenticationException {
public MyAuthenticationException(String msg) {
super(msg);
}
}
两个异常都要重写对应的方法
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 授权异常处理(主要用户权限校验异常)
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
Map<String,Object> map = new HashMap<String,Object>();
map.put("code", HttpStatus.FORBIDDEN.value());
map.put("msg", "权限不足");
map.put("data", accessDeniedException.getMessage());
map.put("success", false);
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(System.currentTimeMillis()));
ObjectMapper mapper = new ObjectMapper();
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(mapper.writeValueAsString(map));
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* 认证异常处理(身份认证异常)
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", HttpStatus.UNAUTHORIZED);
map.put("msg", "登录后才可访问");
map.put("data", authException.getMessage());
map.put("success", false);
map.put("path", request.getServletPath());
map.put("timestamp", String.valueOf(System.currentTimeMillis()));
ObjectMapper mapper = new ObjectMapper();
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(mapper.writeValueAsString(map));
}
}
页面代码是这样
<div class="panel-body">
<form action="/auth/login" method="POST">
<div class="form-group" style="margin-top: 30px">
<div class="input-group col-md-6 col-md-offset-3">
<div class="input-group-addon"><span class="glyphicon glyphicon-user"></span></div>
<input type="text" class="form-control" name="username" id="username" placeholder="账号">
</div>
</div>
<div class="form-group">
<div class="input-group col-md-6 col-md-offset-3">
<div class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></div>
<input type="password" class="form-control" name="password" id="password"
placeholder="密码">
</div>
</div>
<div class="form-group">
<div class="input-group col-md-6 col-md-offset-3" style="text-align: center">
<input type="text" style="width: 120px;" class="form-control" name="verifyCode" required="required" placeholder="验证码">
<img src="getVerifyCode" title="刷新" onclick="refresh(this)" onmouseover="mouseover(this)" />
</div>
</div>
<br>
<div class="form-group">
<div class="input-group col-md-6 col-md-offset-3 col-xs-12 ">
<button type="submit" class="btn btn-primary btn-block">登录</button>
</div>
</div>
<div class="form-group">
<div class="input-group col-md-6 col-md-offset-3" style="text-align: center">
<a href="/register">创建账号</a>
</div>
</div>
</form>
</div>
<script language="JavaScript">
function refresh(obj) {
obj.src = "getVerifyCode?" + Math.random();
}
function mouseover(obj) {
obj.style.cursor = "pointer";
}
</script>
大功告成~