SpringBoot项目实战(008)Spring Security(二)JWT

SpringSecurity && JWT

上一章SpringBoot项目实战(007)Spring Security(一)中,实现了Spring Security的数据库认证。本章采用JWT实现无状态服务的认证和鉴权。

改造流程

  1. 服务改为STATELESS,不再使用session
  2. 数据库中Users表增加token,相应代码调整。后期可以改为token存在redis中。
  3. 新增一个JwtUtils,封装常用的jwt操作
  4. 初次请求登录时,获得一个新的jwttoken,并存入数据库。
  5. 再次请求API时,解析jwttoken,获得用户名,再从数据库载入权限。

无状态服务

现在微服务盛行,大部分RESTFUL API都是采用STATELESS的方式。比如在WebSecurityConfigurerAdapter中:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    ......
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    ......
    }
    ......
}

加入以上代码后,即便你在login页面完成登录,也会在其他需要认证的页面弹出401,这是因为认证成功的SESSION并没有被保留。所以我们需要通过一个Token来传递信息。

数据库及mybatis调整

数据库新增字段Token

CREATE TABLE `Users` (
  `UserId` int(11) NOT NULL AUTO_INCREMENT,
  `UserName` varchar(45) NOT NULL,
  `PassWord` varchar(100) NOT NULL,
  `LockedFlag` tinyint(4) NOT NULL,
  `Token` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`UserId`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

Bean

对应的,bean中添加一个字段:

//UserBean
@Data
@Accessors(chain = true)
@SuppressWarnings("serial")
public class UserBean implements Serializable {
    
    
    private int  userId;
    private String userName;
    private String passWord;
    private int lockedFlag;
    private String token;
}
//UserCondition
@Data
@Accessors(chain = true)
public class UserCondition extends BaseCondition {
    
    
    private int  userId;
    private String userName;
    private String passWord;
    private int lockedFlag;
    private String token;

    @Override
    public Class<?> getChildClass() {
    
    
        return UserBean.class;
    }
}

controller dao service

对应的controller(用于测试)、dao、service中增加方法getUserByToken:

//usercontroller
@RestController
@RequestMapping(value = "/user")
public class UserController extends BaseController<UserBean,UserCondition,IUserService>{
    
    
    ......
    @RequestMapping(value = "/token/{token}", method = RequestMethod.GET)
    public UserBean getUserByToken(@PathVariable(value = "token") String token) {
    
    
        return baseService.getUserByToken(token);
    }
}

//IUserService
public interface IUserService extends IBaseService<UserBean,UserCondition>  {
    
    
    UserBean findByName(String username);
    UserBean getUserByToken(String token);
}

//UserServiceImpl
@Service
public class UserServiceImpl implements IUserService {
    
    
    ......
    @Override
    public UserBean getUserByToken(String token) {
    
    
        return userDao.getUserByToken(token);
    }
}

//userdao
public interface UserDao extends IBaseDao<UserBean,UserCondition> {
    
    
    UserBean findByName(@Param("username") String username);
    UserBean getUserByToken(@Param("token") String token);
}

mybatis.xml

部分方法增加token返回,同时新增getUserByToken方法:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.it_laowu.springbootstudy.springbootstudydemo.dao.UserDao">
    <resultMap id="UserResultMap" type="com.it_laowu.springbootstudy.springbootstudydemo.bean.UserBean">
        <result column="userId" property="userId"/>
        <result column="userName" property="userName"/>
        <result column="passWord" property="passWord"/>
        <result column="lockedFlag" property="lockedFlag"/>
        <result column="token" property="token"/>
    </resultMap>

    <select id="findAll" resultMap="UserResultMap">
        select userId,userName,`passWord`,lockedFlag,token
        from `Users`
        <where>
            <if test="conditionQC.userId != 0">
                and userId = #{conditionQC.userId}
            </if>
            <if test="conditionQC.userName != null and '' != conditionQC.userName">
                and userName like concat('%',#{conditionQC.userName},'%')
            </if>
            <if test="conditionQC.passWord != null and '' != conditionQC.passWord">
                and passWord like concat('%',#{conditionQC.passWord},'%')
            </if>
            <if test="conditionQC.lockedFlag != -1">
                and lockedFlag = #{conditionQC.lockedFlag}
            </if>
            <if test="conditionQC.token != null and '' != conditionQC.token">
                and token like concat('%',#{conditionQC.token},'%')
            </if>
        </where>
        <choose>
            <when test="conditionQC.sortSql == null">
                Order by userId
            </when>
            <otherwise>
                ${conditionQC.sortSql}
            </otherwise>
        </choose>
    </select>

    <select id="findOne" resultMap="UserResultMap">
        select userId,userName,`passWord`,lockedFlag,token
        from `Users`
        where userId = #{keyId}
    </select>
    <select id="findByName" resultMap="UserResultMap">
        select userId,userName,`passWord`,lockedFlag,token
        from `Users`
        where userName = #{username}
    </select>
    <select id="getUserByToken" resultMap="UserResultMap">
        select userId,userName,`passWord`,lockedFlag,token
        from `Users`
        where token = #{token}
    </select>  
    <insert id="insert" parameterType="com.it_laowu.springbootstudy.springbootstudydemo.bean.UserBean">
        insert into `Users`(userId,`userName`,`passWord`,lockedFlag,token)
        values(#{userId},#{userName},#{passWord},#{lockedFlag},#{token})
    </insert>

    <update id="update" parameterType="com.it_laowu.springbootstudy.springbootstudydemo.bean.UserBean">
        update `Users`
        <set>
            <if test="userName!=null"> `userName`=#{userName}, </if>
            <if test="passWord!=null"> `passWord`=#{passWord}, </if>
            <if test="lockedFlag!=null"> `lockedFlag`=#{lockedFlag}, </if>
            <if test="token!=null"> `token`=#{token}, </if>
        </set>
        where userId = #{userId}
    </update>

    <delete id="delete" parameterType="int">
        delete from `Users` where userId = #{keyId}
    </delete>

</mapper>

postman验证

用户列表
用户列表
更新用户
更新用户
根据token获得用户
根据token获得用户

核心代码

pom

引入依赖jar包,这里多用了个hutool,不是必须的,但是有兴趣可以了解下。

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-core</artifactId>
        <version>5.3.6</version>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>

配置文件

application-dev.yml新增一些属性:

扫描二维码关注公众号,回复: 11753779 查看本文章
jwt:
    secret: "abcdefg"
    expiration: 1800
    token-head: "Bearer "
    header-name: "Authorization"

同时,新增一个jwtProperties类:

package com.it_laowu.springbootstudy.springbootstudydemo.core.auth;
......

@Component
@ConfigurationProperties(prefix="jwt")
@Data
public class JwtProperties {
    
    
    private String secret;
    private Long expiration;
    private String tokenHead;
    private String headerName ="Authorization";
}

JwtTokenUtils

涉及到token的一个utils,注意这里没有刷新token,这个细节,有空再完善 。

package com.it_laowu.springbootstudy.springbootstudydemo.core.auth;
......
@Component
public class JwtTokenUtil {
    
    
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";
    private static final String secret ="abcdefg";
    private static final Long expiration=1800L;
    private static final String tokenHead="Bearer ";

    //根据用户信息生成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);
    }
    // 根据权限生成JWT的token
    private String generateToken(Map<String, Object> claims) {
    
    
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
    // token中解出用户名
    public String getUserNameFromToken(String token) {
    
    
        String username;
        try {
    
    
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
    
    
            username = null;
        }
        return username;
    }
    //token中解出claims
    private Claims getClaimsFromToken(String token) {
    
    
        Claims claims = null;
        try {
    
    
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
    
    
        }
        return claims;
    }

    private Date generateExpirationDate() {
    
    
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    public boolean validateToken(String token, UserDetails userDetails) {
    
    
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
    
    
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.before(new Date());
    }

    private Date getExpiredDateFromToken(String token) {
    
    
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }
}

认证器 MyAuthenticationProvider

调整我们的认证代码,使得用户登录时,生成一个新的token,并保存到mysql即可。

package com.it_laowu.springbootstudy.springbootstudydemo.core.auth;
......
@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
    
    
......
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
......
        logger.info(String.format("用户%s登录成功", username));
        // 生成一个新token
        String token = jwtTokenUtil.generateToken(user);
        // 需要持久化的话,那就将token保存到数据库,当然保存到redis更好
        UserBean bean = userService.findByName(username);
        bean.setToken(token);
        userService.update(bean);
        // 绑定到当前用户
        user.setToken(token);
......
    }
}

过滤器 JwtokenAuthenticationFilter

需要新增一个过滤器,在认证器MyAuthenticationProvider之前,判断是否有token,所以我们的过滤器加的位置,在UsernamePasswordAuthenticationFilter之前。
这样假如我们token解析成功,直接生成一个UsernamePasswordAuthenticationToken,加到SecurityContextHolder即可。

首先,调整WebSecurityConfig

package com.it_laowu.springbootstudy.springbootstudydemo.core.config;
......
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
......
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
......
        // 无状态服务
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
     }
}

然后新增过滤器JwtokenAuthenticationFilter

package com.it_laowu.springbootstudy.springbootstudydemo.core.auth;
......
@Component
public class JwtokenAuthenticationFilter extends OncePerRequestFilter {
    
    
    String headerName = "Authorization";
    @Autowired
    private JwtProperties jwtProperties;
    @Resource
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    JwtTokenUtil jwtTokenUtil;

    // 将token转为用户密码的权限方式
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
    
    
        // 取出auth
        String authHeader = request.getHeader(jwtProperties.getHeaderName());

        if (authHeader != null && authHeader.startsWith(jwtProperties.getTokenHead())) {
    
    
            // tokenBody = jwttoken
            String tokenBody = authHeader.substring(jwtProperties.getTokenHead().length());
            if (tokenBody != null) {
    
    
                // 没过期
                String username = jwtTokenUtil.getUserNameFromToken(tokenBody);
                boolean isTokenExpired = jwtTokenUtil.isTokenExpired(tokenBody);
                if (username != null && !isTokenExpired && SecurityContextHolder.getContext().getAuthentication() == null) {
    
    
                    // 根据用户名,读取权限明细
                    UserDetails userDetails = (MyUserDetails) myUserDetailsService.loadUserByUsername(username);
                    if (jwtTokenUtil.isTokenSameUser(tokenBody, userDetails.getUsername())) {
    
    
                        // 生成authentication,
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

postman测试流程

login

首先,修改MyAuthenticationSuccessHandler,使得登录返回token

package com.it_laowu.springbootstudy.springbootstudydemo.core.auth;
......
@Component
public class MyAuthenticationSuccessHandler  extends SavedRequestAwareAuthenticationSuccessHandler{
    
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
    
    
        //登录成功返回
        String token = ((MyUserDetails) authentication.getPrincipal()).getToken();
        ResultBody resultBody = new ResultBody("200", "登录成功:"+token);
......
    }
}

测试结果:

login返回token

如果跟踪一下,会发现先跑到jwt解析,失败后进入认证器。

用token访问api

直接使用刚才返回的token,调用某个api,如果跟踪代码,可以发现token认证成功,没有再进入认证器。

admininfo with token

猜你喜欢

转载自blog.csdn.net/weixin_36572983/article/details/106648819