SpringSecurity && JWT
上一章SpringBoot項(xiàng)目實(shí)戰(zhàn)(007)Spring Security(一)中,實(shí)現(xiàn)了Spring Security
的數(shù)據(jù)庫(kù)認(rèn)證蕴纳。本章采用JWT實(shí)現(xiàn)無(wú)狀態(tài)服務(wù)的認(rèn)證和鑒權(quán)柱衔。
改造流程
- 服務(wù)改為STATELESS肥橙,不再使用session
- 數(shù)據(jù)庫(kù)中
Users
表增加token
窄做,相應(yīng)代碼調(diào)整。后期可以改為token存在redis中械拍。 - 新增一個(gè)
JwtUtils
耸别,封裝常用的jwt
操作 - 初次請(qǐng)求登錄時(shí),獲得一個(gè)新的
jwttoken
奸汇,并存入數(shù)據(jù)庫(kù)施符。 - 再次請(qǐng)求API時(shí),解析
jwttoken
擂找,獲得用戶名戳吝,再?gòu)臄?shù)據(jù)庫(kù)載入權(quán)限。
無(wú)狀態(tài)服務(wù)
現(xiàn)在微服務(wù)盛行贯涎,大部分RESTFUL API
都是采用STATELESS
的方式听哭。比如在WebSecurityConfigurerAdapter
中:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
......
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
......
}
......
}
加入以上代碼后,即便你在login頁(yè)面完成登錄塘雳,也會(huì)在其他需要認(rèn)證的頁(yè)面彈出401陆盘,這是因?yàn)檎J(rèn)證成功的SESSION并沒有被保留。所以我們需要通過(guò)一個(gè)Token來(lái)傳遞信息粉捻。
數(shù)據(jù)庫(kù)及mybatis調(diào)整
數(shù)據(jù)庫(kù)新增字段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
對(duì)應(yīng)的礁遣,bean中添加一個(gè)字段:
//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
對(duì)應(yīng)的controller(用于測(cè)試)、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返回祟霍,同時(shí)新增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驗(yàn)證
用戶列表
更新用戶
根據(jù)token獲得用戶
核心代碼
pom
引入依賴jar包,這里多用了個(gè)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"
同時(shí)呢燥,新增一個(gè)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的一個(gè)utils崭添,注意這里沒有刷新token,這個(gè)細(xì)節(jié)叛氨,有空再完善 呼渣。
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 ";
//根據(jù)用戶信息生成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);
}
// 根據(jù)權(quán)限生成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();
}
}
認(rèn)證器 MyAuthenticationProvider
調(diào)整我們的認(rèn)證代碼,使得用戶登錄時(shí)寞埠,生成一個(gè)新的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));
// 生成一個(gè)新token
String token = jwtTokenUtil.generateToken(user);
// 需要持久化的話仁连,那就將token保存到數(shù)據(jù)庫(kù)蓝角,當(dāng)然保存到redis更好
UserBean bean = userService.findByName(username);
bean.setToken(token);
userService.update(bean);
// 綁定到當(dāng)前用戶
user.setToken(token);
......
}
}
過(guò)濾器 JwtokenAuthenticationFilter
需要新增一個(gè)過(guò)濾器,在認(rèn)證器MyAuthenticationProvider
之前,判斷是否有token使鹅,所以我們的過(guò)濾器加的位置揪阶,在UsernamePasswordAuthenticationFilter
之前。
這樣假如我們token解析成功患朱,直接生成一個(gè)UsernamePasswordAuthenticationToken
鲁僚,加到SecurityContextHolder
即可。
首先麦乞,調(diào)整WebSecurityConfig
:
package com.it_laowu.springbootstudy.springbootstudydemo.core.config;
......
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
......
@Override
protected void configure(HttpSecurity http) throws Exception {
......
// 無(wú)狀態(tài)服務(wù)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
然后新增過(guò)濾器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轉(zhuǎn)為用戶密碼的權(quán)限方式
@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) {
// 沒過(guò)期
String username = jwtTokenUtil.getUserNameFromToken(tokenBody);
boolean isTokenExpired = jwtTokenUtil.isTokenExpired(tokenBody);
if (username != null && !isTokenExpired && SecurityContextHolder.getContext().getAuthentication() == null) {
// 根據(jù)用戶名蕴茴,讀取權(quán)限明細(xì)
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測(cè)試流程
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);
......
}
}
測(cè)試結(jié)果:
如果跟蹤一下,會(huì)發(fā)現(xiàn)先跑到jwt解析
声畏,失敗后進(jìn)入認(rèn)證器撞叽。
用token訪問(wèn)api
直接使用剛才返回的token,調(diào)用某個(gè)api插龄,如果跟蹤代碼愿棋,可以發(fā)現(xiàn)token認(rèn)證成功,沒有再進(jìn)入認(rèn)證器均牢。