spring boot 单体项目 集成 spring security 实现 登录认证 权限认证 jwt token认证

这篇博文讲述的是不集成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>

大功告成~

稍后补充下git地址

https://gitee.com/zhangchai/springboot_springsecurity_mybatispluse.git

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值