08 spring security 之 jwt token

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)柱衔。

改造流程

  1. 服務(wù)改為STATELESS肥橙,不再使用session
  2. 數(shù)據(jù)庫(kù)中Users表增加token窄做,相應(yīng)代碼調(diào)整。后期可以改為token存在redis中械拍。
  3. 新增一個(gè)JwtUtils耸别,封裝常用的jwt操作
  4. 初次請(qǐng)求登錄時(shí),獲得一個(gè)新的jwttoken奸汇,并存入數(shù)據(jù)庫(kù)施符。
  5. 再次請(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獲得用戶
根據(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é)果:

login返回token

如果跟蹤一下,會(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)證器均牢。

admininfo with token
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末糠雨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子徘跪,更是在濱河造成了極大的恐慌甘邀,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垮庐,死亡現(xiàn)場(chǎng)離奇詭異松邪,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)哨查,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門逗抑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人寒亥,你說(shuō)我怎么就攤上這事邮府。” “怎么了溉奕?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵褂傀,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我腐宋,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任胸竞,我火速辦了婚禮欺嗤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘卫枝。我一直安慰自己煎饼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布校赤。 她就那樣靜靜地躺著吆玖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪马篮。 梳的紋絲不亂的頭發(fā)上沾乘,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音浑测,去河邊找鬼翅阵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛迁央,可吹牛的內(nèi)容都是我干的掷匠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岖圈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼讹语!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蜂科,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤顽决,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后崇摄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體擎值,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年逐抑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鸠儿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厕氨,死狀恐怖进每,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情命斧,我是刑警寧澤田晚,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站国葬,受9級(jí)特大地震影響贤徒,放射性物質(zhì)發(fā)生泄漏芹壕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一接奈、第九天 我趴在偏房一處隱蔽的房頂上張望踢涌。 院中可真熱鬧,春花似錦序宦、人聲如沸睁壁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)潘明。三九已至,卻和暖如春秕噪,著一層夾襖步出監(jiān)牢的瞬間钳降,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工巢价, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牲阁,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓壤躲,卻偏偏與公主長(zhǎng)得像城菊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碉克,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容