文章目录
SpringSecurity && JWT
上一章SpringBoot项目实战(007)Spring Security(一)中,实现了Spring Security
的数据库认证。本章采用JWT实现无状态服务的认证和鉴权。
改造流程
- 服务改为STATELESS,不再使用session
- 数据库中
Users
表增加token
,相应代码调整。后期可以改为token存在redis中。 - 新增一个
JwtUtils
,封装常用的jwt
操作 - 初次请求登录时,获得一个新的
jwttoken
,并存入数据库。 - 再次请求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获得用户
核心代码
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新增一些属性:
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);
......
}
}
测试结果:
如果跟踪一下,会发现先跑到jwt解析
,失败后进入认证器。
用token访问api
直接使用刚才返回的token,调用某个api,如果跟踪代码,可以发现token认证成功,没有再进入认证器。