ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

SpringSecurity+自定义登录界面+JwtToken+拦截器+前后端分离

2022-01-23 19:31:12  阅读:180  来源: 互联网

标签:拦截器 return String 自定义 springframework SpringSecurity token org import


1:项目背景:摸鱼之余,准备自己写一个项目,巩固一下自己。在码项目的过程中出现了一些很cd的问题,大家可能也碰到过,再次分享一下我的总结。想用springcloud+vue写一个前后端分离的项目,由于我还没有学vue,哈哈哈啊哈,所以在此用html页面做了一个简单的登录页面模仿安全登录。哈哈哈哈哈。         

2:碰到的问题:自定义了登录页面,而且自定义了登录表单提交的路径(请求到controller层的登录接口),点击提交,请求就是不进入自定义的登录接口中,而是直接走入了实现了UserDetailService的方法。

(后来我重新写了一下MySercurityConfig中的configure方法中的http配置,

    还有就是之前用了下面这个方法,登陆的时候去调用,然后发现总是走不到自己定义的实现了        UserDetailsService的UserDetialServiceImpl方法中,后来就把这个方法删了没用。这个其实就      是根据用户名获取用户信息的方法)

@Overrite
@Bean
public UserDeailService userDeailService() {
    return username -> {
        Admin admin = adminService.getAdminService(username);
        return admin;
    }
    return null;
}

还有就是重写拦截器,生成token校验登录。

下面给大家详细的分析一下我的登录模块

3:导入依赖: 版本选择自己合适的版本 我用的是2.3.7.RELEASE

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

配置文件

jwt:
  #JWT 存储的请求头
  tokenHeader: Authorization #key
  #JWT 加解密使用的签名
  secret: handsome
  #JWT 失效时间
  expiration: 3600
  #JWT 负载中拿到开头
  tokenHead: Bearer #value开头

 4:SpringSecurity初体验:引入依赖以后直接启动项目,访问你的项目路径就会进入到SpringSecurity自带的安全登录页面,用户名:user,密码:在项目启动后的控制台里会打印出来,可以直接登录。但是大家一般不会用SpringSecurity自带的登录页面。

5:自定义登录页面:接下来我们创建一个类继承WebSecurityConfigurerAdapter来实现登录页面自定义 代码如下。

需要先写一个登录界面:可以用html先写一个login.html,放在resourse下的static下面,再写一个controller层的登录接口,然后写一下配置类实现自定义登录页面。

package com.power.auth.securityconfig;

import com.power.auth.filter.LoginFilter;
import com.power.auth.filter.RestAuthorizationEntryPoint;
import com.power.auth.filter.RestfulAccessDenieHandler;
import com.power.auth.utils.RealPasswordEncoding;
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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author HandsomeZhang
 * @date 2021/12/13 15:51
 * Description:
 */
@Configuration//配置类注解,项目启动的时候就会加载该配置类,可以打个断点启动看看
public class MySercurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()//请求授权
                .antMatchers("/loginController/getToken").permitAll()//这个是自定义表单的提交路径,也就是我们的controller层的登录接口,允许访问
                .antMatchers("/login.html").permitAll()//自定义登录界面,设为允许访问
                .anyRequest().authenticated()//其余请求均拦截
                .and()
                //自定义登录表单
                .formLogin()
                .loginPage("/login.html")//登录表单,但是如果不在上面授权访问不了
                .loginProcessingUrl("/loginController/getToken/")//表单提交路径
                .and()
                .httpBasic()
                .and().csrf().disable();//关闭防火墙
    }
}

 此时启动项目,访问项目地址,就会来到自定义的登录页面,而且是,不管你访问项目下的哪个请求路径,都会跳转到登录页面,但是加了拦截器后就不是了,需要访问登录界面,可以解决,后面说。对于非登录状态下的接口想放开的话,可以设置白名单(这个目前还没有用到)。

6:定义一个JwtToken工具类:可以生成校验token

package com.power.auth.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT工具类
 */
@Component
public class JwtTokenUtil {
    private static final String CLAIM_KEY_USERNAME="sub";//claims会根据这个sub,解析token的时候拿到用户名,只能填写sub
    private static final String CLAIM_KEY_CREATED="created";//创建时间
    @Value("${jwt.secret}")
    private String secret;//加解密使用的密钥
    @Value("${jwt.expiration}")
    private Integer expiration;//有效时间

    /**
     * 根据用户信息生成token
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails) {//是如何拿到的用户信息?
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());//拿到用户名
        claims.put(CLAIM_KEY_CREATED, new Date());//登陆时间
        return generateToken(claims);
    }

    /**
     * 根据token获取登录用户名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token) {
        String userName;

        try {
            Claims claims = getClaimsFromToken(token);//拿到荷载
            userName = claims.getSubject();//拿到用户名
        } catch (Exception e) {
            userName = null;
        }
        return userName;
    }

    /**
     * 判断token是否有效
     * @param token
     * @return
     *  用户名不正确,或者token过期都视为无效
     *  返回true为有效
     */
    public boolean tokenExpired(String token, UserDetails userDetails) {
        String userName = getUserNameFromToken(token);
        return userName.equals(userDetails.getUsername()) && isTokenExpired(token);
    }

    /**
     * 判断token 能否被刷新
     * 如果token过期了则可以被刷新
     * @param token
     * @return
     *         false为过期
     */
    public boolean isRefresh(String token) {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token过期时间
     * @param token
     * @return
     */
    public String refreshTokenExpiredTime(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());//新的登陆时间
        return generateToken(claims);
    }

    /**
     * 得到token过期时间
     * @param token
     * @return
     */
    private boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();//获取的是生成token时放进去的失效日期
        return new Date().before(expiration);
    }

    /**
     * 根据token拿到荷载
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)//密钥
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 根据荷载生成JWT token
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        byte[] secretKey = secret.getBytes();
        //生成token
        return Jwts.builder()
                .setClaims(claims)//荷载
                .setExpiration(generateExpirationDate())//失效时间
                .signWith(SignatureAlgorithm.HS512, secretKey)
                .compact();
    }

    /**
     * 生成token失效时间
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(120000);//过期时间两个小时
    }
}

7:我这里重写了sercurity的密码加密规则,我这里使用的是md5工具类,网上可以找到

package com.power.auth.utils;

import com.power.common.utils.MD5Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author HandsomeZhang
 * @date 2021/12/12 13:08
 * Description:
 */
public class RealPasswordEncoding implements PasswordEncoder {
    public static Logger logger = LoggerFactory.getLogger(RealPasswordEncoding.class);

    /**
     * 密码加密
     * @param charSequence
     * @return
     */
    @Override
    public String encode(CharSequence charSequence) {
        logger.info("自定义密码加密>>>>>>>>");

        String passwordSafe = null;
        try {
            String password = (String) charSequence;
            passwordSafe = MD5Utils.md5Encode(password);
        } catch (Exception e) {
            logger.info("加密失败");
            e.printStackTrace();
        }
        return passwordSafe;
    }

    /**
     * 密码校验
     * @param charSequence 未加密密码
     * @param s 已加密密码
     * @return
     */
    @Override
    public boolean matches(CharSequence charSequence, String s) {
        logger.info("自定义密码校验>>>>>>>>");

        String passwordSafe = null;
        try {
            String password = (String) charSequence;
            passwordSafe = MD5Utils.md5Encode(password);
            String s1 = MD5Utils.md5Encode("Zjg123...");
            logger.info(s1);
        } catch (Exception e) {
            logger.info("加密失败");
            e.printStackTrace();
        }
        logger.info(passwordSafe);

        String password = (String) charSequence;
        boolean b = MD5Utils.matches(password, s);
        if (b == true) {
            logger.info("密码比对成功");
        } else {
            logger.info("密码比对失败");
        }
        return b;
    }
}

8:定义一个UserDetails的实现类代替springsecurity自带的User,方便接收自己想要的参数

package com.power.auth.entity;

import com.power.basic.entity.User;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;

/**
 * 自定义LoginUserDetials取代User可以放更多的信息
 */

@Data
@NoArgsConstructor
public class LoginUserDetials implements UserDetails, Serializable {
    private Long id;
    private String username;
    private String password;

    private Collection<? extends GrantedAuthority> authorities;
    private boolean isAccountNonExpired;
    private boolean isAccountNonLocked;
    private boolean isCredentialsNonExpired;
    private boolean isEnabled;

    public LoginUserDetials(User user, Collection<? extends GrantedAuthority> authorities) {
        this.setUsername(user.getUserName());
        this.setId(user.getUserId());
        this.setPassword(user.getUserPassword());
        this.setAuthorities(authorities);
        this.setAccountNonExpired(true);
        this.setAccountNonLocked(true);
        this.setCredentialsNonExpired(true);
        this.setEnabled(true);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }


    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

}

9:校验登录用户信息,实现security自带的UserDetailsService接口

package com.power.auth.serviceimpl;

import com.alibaba.fastjson.JSONObject;
import com.power.auth.entity.LoginUserDetials;
import com.power.auth.utils.JwtTokenUtil;
import com.power.basic.api.UserServiceFeign;
import com.power.basic.entity.User;
import com.power.common.pojo.ResultPublic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
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.HashMap;
import java.util.Map;

/**
 * @author HandsomeZhang
 * @date 2021/12/13 16:57
 * Description:
 */
@Service
public class UserDetialServiceImpl implements UserDetailsService {
    //从数据库
    @Autowired(required = false)
    private UserServiceFeign userServiceFeign;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    //校验登录用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("userName", username);
        //查询数据判断用户名是否存在
        ResultPublic<User> getUser = userServiceFeign.getByUserName(jsonObject);
        User user = getUser.getData();
        if (user == null ) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        //把查询出来的密码进行解析,注册时已经进行过加密
        LoginUserDetials loginUserDetials = new LoginUserDetials(user,
                AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));

        //判断用户状态是否正常
        if (!loginUserDetials.isEnabled()) {
            throw new DisabledException("该账户已被禁用!");
        } else if (!loginUserDetials.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定!");
        }
        return loginUserDetials;
    }

    //登录时创建token
    public ResultPublic createToken (UserDetails userDetails, String password) {
        if (null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) {
            return ResultPublic.fail("用户名或者密码不正确");
        }
        //登陆对象放进spring security全文中,不放可能会出现一些问题
        //更新security登录用户对象
        UsernamePasswordAuthenticationToken authenticationToken = new
                UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());//参数:用户信息,密码,权限列表,
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);//将token放入Security全局中
        //生成token
        Map<String, Object> tokenMap = new HashMap<>();
        String token = jwtTokenUtil.generateToken(userDetails);
        tokenMap.put("token", token);//放入token
        tokenMap.put("tokenHead", tokenHead);//放入Bearer
        //登录成功后,前端会将map中的两个值拼接后,以后请求都会带在请求头中
        return ResultPublic.success("登录成功", tokenMap);
    }
}

10:定义一个登录接口:每次登录,都会生成一个token

package com.power.auth.controller;

import com.power.auth.serviceimpl.UserDetialServiceImpl;
import com.power.common.pojo.ResultPublic;
import io.swagger.annotations.Api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

@Api(tags = "LoginController")
@RestController
@RequestMapping(path = "/loginController")
public class LoginController {
    @Resource
    private UserDetailsService userDetailsService;
    @Autowired
    private UserDetialServiceImpl userDetialServiceImpl;

    public static Logger logger = LoggerFactory.getLogger(LoginController.class);

    @PostMapping(path = "/getToken")
    public ResultPublic getToken(@RequestParam String username,
                           @RequestParam String password,
                            HttpServletRequest request
    ) {
        //数据库查询比对登录用户状态信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        //生成token
        ResultPublic token = userDetialServiceImpl.createToken(userDetails, password);
        return token;
    }

    @GetMapping(path = "/loginOut")
    public ResultPublic loginOut() {
        return ResultPublic.success("成功退出");
    }

}

11:定义一个拦截器,每次访问接口的时候,都会经过该拦截器,验证token是否失效等

package com.power.auth.filter;

import com.power.auth.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author HandsomeZhang
 * @date 2021/12/13 20:39
 * Description:
 */

public class LoginFilter extends OncePerRequestFilter {
    @Value("${jwt.tokenHeader}")
    private String tokenHeader;//前端带来的请求头
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    public static Logger logger = LoggerFactory.getLogger(LoginFilter.class);

    //每次访问都来到这里拦截并校验token
    //如果存在token并且用户状态正常的话,但是token过期,则重新设置用户对象
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        logger.info("进入拦截器JwtAuthencationTokenFilter!!!");

        //前端传来的token 例:Authorization = Bearer+jwt工具类生成的令牌
        String fakeToken = request.getHeader(tokenHeader);
        //如果token不为空,并且token以Bearer开头
        if (null != fakeToken && fakeToken.startsWith(tokenHead)) {
            String authToken = fakeToken.substring(tokenHeader.length());//截取掉Bearer拿到后面的token
            String userName = jwtTokenUtil.getUserNameFromToken(authToken);//从token中获取用户名
            //token存在用户名但未登录
            if (null != userName && null == SecurityContextHolder.getContext().getAuthentication()) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(userName);//根据用户名查的用户信息
                //验证token是否过期,重新设置用户对象
                if (jwtTokenUtil.tokenExpired(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authenticationToken = new
                            UsernamePasswordAuthenticationToken(userDetails, null,userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        logger.info("JwtAuthencationTokenFilter拦截器放行!!!");
        filterChain.doFilter(request, response);
    }
}

12:还可以自己定义两个异常处理方案:我这里定义了RestAuthorizationEntryPoint和RestfulAccessDenieHandler在MySercurityConfig里面使用

标签:拦截器,return,String,自定义,springframework,SpringSecurity,token,org,import
来源: https://blog.csdn.net/qq_47737949/article/details/121916710

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有