spring-boot+mybatis+restful api+jwt登陸(2)

前文回顧spring-boot+mybatis+restful api+jwt登陸(1)

用spring-boot開發(fā)RESTful API非常的方便腻格,在生產(chǎn)環(huán)境中悄谐,對發(fā)布的API增加授權(quán)保護(hù)是非常必要的。現(xiàn)在我們來看如何利用JWT技術(shù)為API增加授權(quán)保護(hù),保證只有獲得授權(quán)的用戶才能夠訪問API凯楔。

1. 引入security和jwt依賴

前文已經(jīng)引入了這兩個包,這里再看一下這兩個是如何引入的

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>

2. 增加注冊功能

利用前文引入的mybatis自動生成代碼的插件锦募,生成model和mapper

  • User類摆屯,省略setter和getter方法
public class User {
    private String id;

    private String username;

    private String password;

    private String email;

    private String mobile;

    private String loginIp;

    private Date loginTime;

    private Byte isAviliable;

    private Integer type;

    private String avatar;
}
  • UserMapper
public interface UserMapper {
    int deleteByPrimaryKey(String id);

    int insert(User record);

    int insertSelective(User record);

    User selectByPrimaryKey(String id);

    int updateByPrimaryKeySelective(User record);

    int updateByPrimaryKey(User record);

    User findByUsername(String username);
}

插件還會生成對應(yīng)的mapper.xml,具體代碼不再貼出了


創(chuàng)建UserService類糠亩,加入signup方法

@Service
public class UserService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    public User signup(User user) {
        user.setId(UUID.randomUUID().toString());
        user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
        userMapper.insertSelective(user);
        return user;
    }
}

加入控制層代碼

@RestController
@RequestMapping("api/user")
public class UserController {

    @Autowired
    UserService userService;

    @PostMapping(value = "/signup")
    public User signup(@RequestBody User user) {
        user = userService.signup(user);
        return user;
    }
}

密碼采用了BCryptPasswordEncoder進(jìn)行加密虐骑,我們在啟動類中增加BCryptPasswordEncoder實例的定義。

@SpringBootApplication
@MapperScan("com.itcuc.qaserver.mapper")
@ServletComponentScan
public class QaserverApplication {

    public static void main(String[] args) {
        SpringApplication.run(QaserverApplication.class, args);
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

3. 增加jwt認(rèn)證功能

用戶填入用戶名密碼后赎线,與數(shù)據(jù)庫里存儲的用戶信息進(jìn)行比對廷没,如果通>過,則認(rèn)證成功氛驮。傳統(tǒng)的方法是在認(rèn)證通過后,創(chuàng)建sesstion济似,并給客戶端返回cookie〗梅希現(xiàn)在我們采用JWT來處理用戶名密碼的認(rèn)證。區(qū)別在于砰蠢,認(rèn)證通過后蓖扑,服務(wù)器生成一個token,將token返回給客戶端台舱,客戶端以后的所有請求都需要在http頭中指定該token律杠。服務(wù)器接收的請求后,會對token的合法性進(jìn)行驗證竞惋。驗證的內(nèi)容包括:

  1. 內(nèi)容是一個正確的JWT格式
  2. 檢查簽名
  3. 檢查claims
  4. 檢查權(quán)限
處理登錄

創(chuàng)建一個類JWTLoginFilter柜去,核心功能是在驗證用戶名密碼正確后,生成一個token拆宛,并將token返回給客戶端:

public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    // 接收并解析用戶憑證
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req,
                                                HttpServletResponse res) throws AuthenticationException {
        try {
            User user = new ObjectMapper()
                    .readValue(req.getInputStream(), User.class);

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            user.getUsername(),
                            user.getPassword(),
                            new ArrayList<>())
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    // 用戶成功登錄后嗓奢,這個方法會被調(diào)用,我們在這個方法里生成token
    @Override
    protected void successfulAuthentication(HttpServletRequest req,
                                            HttpServletResponse res,
                                            FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {

        String token = Jwts.builder()
                .setSubject(((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
                .signWith(SignatureAlgorithm.HS512, "MyJwtSecret")
                .compact();
        res.addHeader("Authorization", "Bearer " + token);
    }

}

該類繼承自UsernamePasswordAuthenticationFilter浑厚,重寫了其中的2個方法:

attemptAuthentication :接收并解析用戶憑證股耽。

successfulAuthentication :用戶成功登錄后,這個方法會被調(diào)用钳幅,我們在這個方法里生成token物蝙。

授權(quán)驗證

用戶一旦登錄成功后,會拿到token敢艰,后續(xù)的請求都會帶著這個token诬乞,服務(wù)端會驗證token的合法性。

創(chuàng)建JWTAuthenticationFilter類,我們在這個類中實現(xiàn)token的校驗功能丽惭。

public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);

    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if (token != null) {
            // parse the token.
            String user = Jwts.parser()
                    .setSigningKey("MyJwtSecret")
                    .parseClaimsJws(token.replace("Bearer ", ""))
                    .getBody()
                    .getSubject();

            if (user != null) {
                return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());
            }
            return null;
        }
        return null;
    }

}

該類繼承自BasicAuthenticationFilter击奶,在doFilterInternal方法中,從http頭的Authorization 項讀取token數(shù)據(jù)责掏,然后用Jwts包提供的方法校驗token的合法性柜砾。如果校驗通過,就認(rèn)為這是一個取得授權(quán)的合法請求换衬。

SpringSecurity配置

通過SpringSecurity的配置痰驱,將上面的方法組合在一起。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public WebSecurityConfig(UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userDetailsService = userDetailsService;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.POST, "/api/user/signup").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JWTAuthenticationFilter(authenticationManager()));
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }

}

這是標(biāo)準(zhǔn)的SpringSecurity配置內(nèi)容瞳浦,就不在詳細(xì)說明担映。注意其中的

.addFilter(new JWTLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()))

這兩行,將我們定義的JWT方法加入SpringSecurity的處理流程中叫潦。

(以上內(nèi)容引用自https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226)

這里需要實現(xiàn)UserDetailsService接口

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private UserMapper userMapper;

    /**
     * 通過構(gòu)造器注入UserRepository
     * @param userMapper
     */
    public UserDetailsServiceImpl(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.findByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException(username);
        }
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), emptyList());
    }

}

這時再請求hello接口蝇完,會返回403錯誤


image.png

下面注冊一個新用戶


image.png

使用新注冊的用戶登錄,會返回token矗蕊,在http header中短蜕,Authorization: Bearer 后面的部分就是token


image.png

然后我們使用這個token再訪問hello接口
image.png

至此,功能完成

參考感謝:

  1. https://blog.csdn.net/sxdtzhaoxinguo/article/details/77965226
  2. https://blog.csdn.net/codejas/article/details/79334545
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末傻咖,一起剝皮案震驚了整個濱河市朋魔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卿操,老刑警劉巖警检,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異害淤,居然都是意外死亡扇雕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門窥摄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洼裤,“玉大人,你說我怎么就攤上這事溪王∪埃” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵莹菱,是天一觀的道長移国。 經(jīng)常有香客問我,道長道伟,這世上最難降的妖魔是什么迹缀? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任使碾,我火速辦了婚禮,結(jié)果婚禮上祝懂,老公的妹妹穿的比我還像新娘票摇。我一直安慰自己,他們只是感情好砚蓬,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布矢门。 她就那樣靜靜地躺著,像睡著了一般灰蛙。 火紅的嫁衣襯著肌膚如雪祟剔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天摩梧,我揣著相機(jī)與錄音物延,去河邊找鬼。 笑死仅父,一個胖子當(dāng)著我的面吹牛叛薯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笙纤,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼耗溜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了粪糙?” 一聲冷哼從身側(cè)響起强霎,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤忿项,失蹤者是張志新(化名)和其女友劉穎蓉冈,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體轩触,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡寞酿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了脱柱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伐弹。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖榨为,靈堂內(nèi)的尸體忽然破棺而出惨好,到底是詐尸還是另有隱情,我是刑警寧澤随闺,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布日川,位于F島的核電站,受9級特大地震影響矩乐,放射性物質(zhì)發(fā)生泄漏龄句。R本人自食惡果不足惜回论,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望分歇。 院中可真熱鬧傀蓉,春花似錦、人聲如沸职抡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽繁调。三九已至萨蚕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蹄胰,已是汗流浹背岳遥。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留裕寨,地道東北人浩蓉。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像宾袜,于是被迫代替她去往敵國和親捻艳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

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