005-云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();
}
}