006-云E办_登录功能

一、依赖、配置、工具类、公共返回对象

采用SpringSecurity安全框架以及JWT令牌实现登录功能。

1、依赖、配置

引入依赖:

<!--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.0</version>
</dependency>

加入yaml配置:
secret:jwt加密以及解密要用到的秘钥

jwt:
  # JWT存储的请求头
 tokenHeader: Authorization
  # JWT 加解密使用的密钥
 secret: yeb-secret
  # JWT的超期限时间(60*60*24)
 expiration: 604800
  # JWT 负载中拿到开头
 tokenHead: Bearer

2、jwt令牌的工具类

方便后边跟jwt的功能而使用。
创建config包,用来放项目要用的配置。
创建JwtTokenUtil

1、根据用户信息生成token
2、从token中获取用户名(获取用户名先 必须先获取荷载)
3、验证token是否失效
4、判断token是否可以刷新
5、刷新token
/**
 * JwtToken工具类
 */
@Component
public class JwtTokenUtil {

    //荷载 用户名的key
    private static final String CLAIM_KEY_USERNAME="sub";
    //jwt的创建时间
    private static final String CLAIM_KEY_CREATED="created";

    //jwt的秘钥以及失效时间,通过刚刚的配置目录去拿。通过value注解
    @Value("${jwt.secret}")
    private String secret;
    //失效时间:
    @Value("${jwt.expiration}")
    private Long expiration;

    /*
        1.根据用户名生成token
        2.根据token拿到用户名
        2.判断token是否失效
        3.判断token是否能被刷新
        4.刷新token
     */



    /**
     * 1根据用户名信息生成token
     */
    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);
    }

    /**
     * 2从token中获取登录用户名
     * @param
     * @param token
     * @return
     *
     * 按键盘上 CTRL +ALT + t  快捷键
     */

    public String getUserNameFromToken(String token){
        String username=null;
        try {
            //先获取荷载,因为登录用户名是放在荷载中的
            Claims claims = getClaimsFromToken(token);
            //拿到荷载,通过荷载拿到登录用户名
            username = claims.getSubject();
        } catch (Exception e) {
            username=null;
        }
        return username;

    }

    /**
     * 2.1从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;
    }

    /**
     * 3判断token是否有效:
     *  1.是否过期
     *  2.token荷载的用户名和userDetails的用户名是否一致。
     * @return
     */
    public boolean validateToken(String token, UserDetails userDetails){
        //获取用户名
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpiration(token);
    }

    /**
     * 4判断token是否能被刷新:
     * isTokenExpiration=true
     *  说明以及过期了,则false. 取反则能刷新了
     * @param token
     * @return
     */
    public boolean canRefresh(String token){
        //如果过期了就可以被刷新
        return !isTokenExpiration(token);
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String RefreshToken(String token){
        //刷新就是把过期时间更改一下:
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED,new Date());
        return generateToken(claims);
    }


    /**
     * 3.1判断token是否失效:
     * @param token
     * @return
     */
    private boolean isTokenExpiration(String token) {
        //先获取失效时间:
        Date exprireDate = getExpiredDateFromToken(token);
        //判断:失效时间:是否在当前时间的前面
        return exprireDate.before(new Date());
    }

    /**
     * 3.2从token中获取失效时间:
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token) {
        //从token中获取荷载,因为过期时间在荷载里面
        Claims claims = getClaimsFromToken(token);
        //过期时间:
        return claims.getExpiration();
    }


    /**
     * 1根据荷载生成Jwt token
     * 参数是荷载。
     */
    private String generateToken(Map<String,Object> claims){
        return Jwts.builder()
                //传入荷载
                .setClaims(claims)
                //设定过期时间
                .setExpiration(generateExpirationDate())
                //签名算法,秘钥
                .signWith(SignatureAlgorithm.ES512,secret)
                .compact();
    }

    /**
     * 1生成token失效时间
     * @return
     */
    private Date generateExpirationDate() {
        //失效时间:当前时间+过期时间
        return new Date(System.currentTimeMillis()+expiration*1000);
    }
    
}

3、公共返回对象

流程:
前端转过来用户名和密码,后端先去校验用户名和密码,如果错误让用户重新输入。如果正确则返回一个jwt令牌,传给前端。前端拿到jwt令牌后放在请求头里面,后面的任何请求都会携带jwt令牌,后端会有拦截器对jwt令牌进行验证,验证通过后,才能访问对应的接口。如果jwt验证不通过,那说明失效了、或者用户名有问题。

后端第一步根据用户名和密码登录后,会返给前端一个令牌。那后面整个项目都会有一些公用的返回东西,那么写一个公共返回对象。

pojo/respBean (公共返回对象)

//lombok:无参构造、全参构造
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {
    //状态码
    private long code;
    //提示信息
    private String message;
    //可能返回一下对象
    private Object obj;
    //可以定义一些返回的东西了:

    /**
     * 成功返回结果
     * @param message
     * @return
     */
    public static RespBean success(String message){
        return new RespBean(200,message,null);
    }

    /**
     * 成功返回结果
     * @param message
     * @param obj
     * @return
     */
    public static RespBean success(String message,Object obj){
        return new RespBean(200,message,obj);
    }

    /**
     * 失败返回的结果
     * @param message
     * @return
     */
    public static RespBean error(String message){
        return new RespBean(500,message,null);
    }

    /**
     * 失败返回结果:
     * @param message
     * @param obj
     * @return
     */
    public static RespBean error(String message,Object obj){
        return new RespBean(500,message,obj);
    }
}

二、登录流程:

1、登录之后返回的token

登录的流程:
前端输入账号和密码传给后端,后端去判断用户和密码是否正确。正确的话就会给生成一个jwt令牌返回给前端。如果不正确,就让用户重新输入。
前端拿到我们生成的jwt令牌之后,就会把令牌放到请求头里面,后面的每一次请求都会携带jwt令牌。我们后端会写一个jwt令牌相关的拦截器,每次请求页面之前都会去拦截器判断jwt令牌是否存在、是否合法有效。如果不存在、合法无效的jwt令牌的话,就会拦截掉,不让访问去接口。如果存在合法有效的jwt令牌的话,就运行访问其他的接口。

判断前端传来的用户名和密码,返回jwt令牌
前端传用户名和密码,后端判断用户名和密码是否正确,正确的话生成jwt令牌

1.pojo/Admin下实现UserDetails:

public class Admin implements Serializable , UserDetails {
    /**
     * 已经是SpringSecurity框架了
     * 真正登录的方法就是UserDetails的Username
     * 登陆成功就是details
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

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

3.pojo/adminLoginParam

/**
 * 用户登录实体类:
 * 专门用来专递前端传给我们的用户名和密码
 * 登录只需要给后端传用户名和密码
 */
//lombok:
@Data
@EqualsAndHashCode(callSuper = false)
//具体的作用是开启链式编程,让我们写代码更加方便。
@Accessors(chain = true)
//接口文档:swagger:
@ApiModel(value = "AdminLogin对象",description = "")
public class adminLoginParam {
    @ApiModelProperty(value = "用户名",required=true)
    private String username;
    @ApiModelProperty(value = "密码",required = true)
    private String password;
}

2.controller/LoginController

@Api(tags = "LoginController")
@RestController
public class LoginController {
    //注入service
    private IAdminService adminService;

    //用swagger注解代表注释了
    @ApiOperation(value = "登录之后返回token")
    @PostMapping("/login")
    public RespBean login(AdminLoginParam adminLoginParam, HttpServletRequest request){
        //service层login登录方法
        return adminService.login(adminLoginParam.getUsername(),adminLoginParam.getPassword(),request);
    }
}

4.service/IAdminService

    /**
     * 登录之后,返回token
     * @param username
     * @param password
     * @param request
     * @return
     */
    RespBean login(String username, String password, HttpServletRequest request);
----实现类:
/**
     * 登录之后返回token
     * @param username
     * @param password
     * @param request
     * @return
     */
    @Override
    public RespBean login(String username, String password, HttpServletRequest request) {
        //security主要是通过:UserDetailsService里面的username来实现登录的
        //将浏览器传过来的username,放进去。 返回的是userDetails用户详细信息(账号、密码、权限等等)
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        //判断传过来的username是否为空 或者 (浏览器输入的和数据库密码不一致) 则密码或者用户名是错的
        if(userDetails==null ||passwordEncoder.matches(password,userDetails.getPassword())){
            return RespBean.error("用户名或者密码不正确");
        }
        //判断是否禁用
        if(!userDetails.isEnabled()){
            return RespBean.error("账号被禁用");
        }
        /**
         * 更新security登录用户对象
         * 参数:userDetails,凭证密码null,权限列表
         *
         * security的全局里面
         */

        UsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken
                (userDetails,null,userDetails.getAuthorities());
        //上下文持有人
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        /**
         * 生成token返回给前端
         * 如果以上都没有进入判断,说明用户和密码是正确的:就可以拿到jwt令牌了:
         * 根据用户信息生成令牌
         */

        String token = jwtTokenUtil.generateToken(userDetails);
        //有了token,就用map返回:
        Map<String,String> tokenMap=new HashMap<>();
        //将token返回去
        tokenMap.put("token",token);
        //头部信息也返回去前端,让他放在请求头里面
        tokenMap.put("tokenHead",tokenHead);
        return RespBean.success("登陆成功",tokenMap);
    }

2、退出功能和获取当前登录用户信息

退出功能和获取当前登录用户信息

退出功能比较简单,前端来处理退出功能。后端只要返回一个成功的接口即可。
主要是前端获取一个状态码:200
前端在请求头里面把token删除,再去调用接口就会被拦截器拦截

1.LoginController

     /**
     * 获取当前登录的用户信息
     */
    @ApiOperation(value = "获取当前用户登录的信息")
    @GetMapping("/admin/info")
    public Admin getAdminInfo(Principal principal){
        if(null==principal){
            return null;
        }
        String username = principal.getName();
        //根据用户名获取完全的用户对象
        Admin admin = adminService.getAdminByUserName(username);
        //但是用户的密码不会返回给浏览器
        admin.setPassword(null);
        return admin;
    }
 
    /**
     * 退出登录
     * @return
     */
    @ApiOperation(value = "退出登录")
    @PostMapping("/logout")
    public RespBean logout(){
        return RespBean.success("注销成功");
    }

2.service/AdminService及实现类:

IAdminService接口

public interface IAdminService extends IService<Admin> {


    /**
     * 根据用户名获取用户:
     * @param username
     * @return
     */
    Admin getAdminByUserName(String username);
}
-----实现类:
    /**
     * 根据用户名获取对象
     * @param username
     * @return
     * 自动注入Mapper 因为去数据库查询了:
     */
    @Override
    public Admin getAdminByUserName(String username) {
        /*
         * 查询一个(泛型是admin。equals(提示:表的字段"username":username));
         * 1.用户名去匹配
         * 2.账户是否禁用
         */
        return adminMapper.selectOne(new QueryWrapper<Admin>()
                .eq("username",username)
                .eq("enabled",true));
    }

3、配置Security登录的授权过滤器

config/security/sercurityConfig

package com.xxxx.server.config.security;

import com.xxxx.server.pojo.Admin;
import com.xxxx.server.service.IAdminService;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
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.UsernamePasswordAuthenticationFilter;

/**
 * SpringSecurity主体的登录逻辑:
 * UserDetails里面的loud把user..实现了
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private IAdminService adminService;

    //由于我们自己重写了UserDetailsService,希望调用的时候调用我们自己重写的方法
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/login","logout")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .headers()
                .cacheControl();

        //添加jwt,登录过滤器
        http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        //添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler()
                .authenticationEntryPoint();
    }

    //重写了userDetailsService
    @Override
    @Bean
    public UserDetailsService userDetailsService(){
    return username ->{
        Admin admin = adminService.getAdminByUserName(username);
        if(null!=admin){
            return admin;
        }
        return null;
    };

    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //bean注解暴露出来
    @Bean
    public JwtAuthencationTokenFilter jwtAuthencationTokenFilter(){
        return new JwtAuthencationTokenFilter();
    }
}

JwtAuthencationTokenFilter

/**
 * JWT 登录授权过滤器
 *
 * 前置拦截
 */

public class JwtAuthencationTokenFilter extends OncePerRequestFilter {
    @Value("${jwt.tokenHender}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    //d登录需要userDetailsService
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        //Header是前端传给我们.
        //要验证的头:
        String authHeader = request.getHeader(tokenHeader);
        //存在token,如果不存在或者开头不是tokenHead

        if(null!=authHeader &&authHeader.startsWith(tokenHead)){
            String authToken = authHeader.substring(tokenHead.length());
            //从token中获取用户名
            String userName = jwtTokenUtil.getUserNameFromToken(authToken);
            //token存在,用户名未登录
            if(null!=userName&& null== SecurityContextHolder.getContext().getAuthentication()){
                //登录了
                UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
                //重新放到用户对象当中:返回是boolean
                if(jwtTokenUtil.validateToken(AuthToken,userDetails)){
                    UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken
                            (userDetails,null,userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        //放行
        filterChain.doFilter(request,httpServletResponse);


    }
}

1.Security自定义返回结果:

RestAuthorizationEntryPoint

/**
 * 当未登录或者token失效访问接口时,自定义的返回结果
 */
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        //设置编码格式:ut-f8
        httpServletResponse.setCharacterEncoding("UTF-8");
        //Json格式
        httpServletResponse.setContentType("application/json");
        PrintWriter out = httpServletResponse.getWriter();
        RespBean bean = RespBean.error("未登录,请登录");
        bean.setCode(401);
        out.flush();
        out.close();
 
    }
}

RestfulAccessDeniedHandler

/**
 * 当权限接口没有权限时,自定义返回结果:
 *
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");

        PrintWriter out = httpServletResponse.getWriter();
        RespBean bean = RespBean.error("权限不足,请联系管理员");
        bean.setCode(403);
        out.write(new ObjectMapper().writeValueAsString(bean));
        out.flush();
        out.close();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值