Spring Security實戰(zhàn)一:登錄注冊

登錄注冊總是密不可分,就放在一起說吧匆绣。數(shù)據(jù)庫中明文密碼自然是不允許的。需要在項目級別攔截請求什黑,來實現(xiàn)登錄注冊操作崎淳。

一、要解決的問題

??本篇要解決的問題

  • 項目級別統(tǒng)一攔截請求
  • 注冊加密
  • 登錄校驗
  • 登錄成功/失敗返回自定義信息
  • 自定義用戶信息

二兑凿、原理

??Spring Boot項目中引入Spring Security凯力,通過WebSecurityConfigurerAdapter來實現(xiàn)請求的統(tǒng)一攔截,攔截到請求后礼华,通過UserDetailsService來查詢數(shù)據(jù)庫中存儲的用戶信息,比對登錄請求傳輸?shù)男畔⑥置兀瑏泶_定登錄成功與否圣絮。

三、實戰(zhàn)

1.引入Spring Security

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.0.1.RELEASE</version>
        </dependency>

2.自定義WebSecurityConfigurerAdapter統(tǒng)一攔截請求

/**
 * @EnableWebSecurity:此注解會啟用Spring Security
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 1)HttpSecurity支持cors雕旨。
     * 2)默認(rèn)會啟用CRSF扮匠,此處因為沒有使用thymeleaf模板(會自動注入_csrf參數(shù))捧请,
     * 要先禁用csrf,否則登錄時需要_csrf參數(shù)棒搜,而導(dǎo)致登錄失敗疹蛉。
     * 3)antMatchers:匹配 "/" 路徑,不需要權(quán)限即可訪問力麸,匹配 "/user" 及其以下所有路徑可款,
     *  都需要 "USER" 權(quán)限
     * 4)配置登錄地址和退出地址
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .and()
                .formLogin().loginPage("/login").defaultSuccessUrl("/hello")
                .and()
                .logout().logoutUrl("/logout").logoutSuccessUrl("/login");
    }
}

3.自定義UserDetailsService查詢數(shù)據(jù)庫中用戶信息

@Service
public class MyUserDetailService implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //從數(shù)據(jù)庫查詢用戶信息
        UserInfoBean userInfo = userService.getUser(username);
        if (userInfo == null){
            throw new UsernameNotFoundException("用戶不存在!");
        }
        //查詢權(quán)限信息
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = createAuthorities(userInfo.getRoles());
        //返回Spring Security框架提供的User或者自定義的MyUser(implements UserDetails)
        ////        return new MyUser(username, userInfo.getPassword(), simpleGrantedAuthorities);
        return new User(username, userInfo.getPassword(), simpleGrantedAuthorities);
    }

    /**
     * 權(quán)限字符串轉(zhuǎn)化
     *
     * 如 "USER,ADMIN" -> SimpleGrantedAuthority("USER") + SimpleGrantedAuthority("ADMIN")
     *
     * @param roleStr 權(quán)限字符串
     */
    private List<SimpleGrantedAuthority> createAuthorities(String roleStr){
        String[] roles = roleStr.split(",");
        List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
        for (String role : roles) {
            simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role));
        }
        return simpleGrantedAuthorities;
    }
}

4.密碼加密

??密碼加密簡單說明下克蚂。密碼加密分兩個部分闺鲸,注冊時給存儲到數(shù)據(jù)庫的密碼加密和登錄驗證時將拿到的密碼加密與數(shù)據(jù)庫中密碼比對。Spring Security提供了幾種加密方式埃叭,當(dāng)然也可自定義摸恍,此處選用BCryptPasswordEncoder。
??BCryptPasswordEncoder相關(guān)知識:用戶表的密碼通常使用MD5等不可逆算法加密后存儲赤屋,為防止彩虹表破解更會先使用一個特定的字符串(如域名)加密立镶,然后再使用一個隨機的salt(鹽值)加密。特定字符串是程序代碼中固定的类早,salt是每個密碼單獨隨機媚媒,一般給用戶表加一個字段單獨存儲,比較麻煩莺奔。BCrypt算法將salt隨機并混入最終加密后的密碼欣范,驗證時也無需單獨提供之前的salt,從而無需單獨處理salt問題令哟。

1)注冊時給密碼加密

注冊接口中加密密碼:

@Service
public class UserService {

    @Autowired
    private UserInfoMapper userInfoMapper;

    public boolean insert(UserInfoBean userInfo){
        encryptPassword(userInfo);
        if(userInfoMapper.insert(userInfo)==1)
            return true;
        else
            return false;
    };

    private void encryptPassword(UserInfoBean userInfo){
        String password = userInfo.getPassword();
        password = new BCryptPasswordEncoder().encode(password);
        userInfo.setPassword(password);
    }
}
2)登錄時密碼加密校驗

只需在WebSecurityConfigurerAdapter的子類中指定密碼的加密規(guī)則即可恼琼,Spring Security會自動將密碼加密后與數(shù)據(jù)庫比對。

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

    /**
     * 密碼加密
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

5.自定義用戶登錄流程

??要實現(xiàn)當(dāng)用戶是html結(jié)尾的請求就跳轉(zhuǎn)到默認(rèn)的登錄頁面或者指定的登錄頁屏富,用戶是restFul請求時就返回json數(shù)據(jù)晴竞。備注:此處的校驗邏輯是以html后綴來校驗,如果集成其他模板引擎可根據(jù)需要修改狠半。
1)先定義實現(xiàn)以上需求的controller邏輯

private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @RequestMapping("/logintype")
    @ResponseBody
    @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
    public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl =  savedRequest.getRedirectUrl();
            logger.info("引發(fā)跳轉(zhuǎn)的請求是:" + targetUrl);
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, securityProperties.getBrower().getLoginPage());
            }
        }
        return "請登錄!";
    }


    @GetMapping("/login_html")
    public String loginHtml(){
        return "login";
    }

    @PostMapping("/login")
    public void login(){
    }

2)定義不同登錄頁面的配置類

@Configuration
@ConfigurationProperties(prefix = "evolutionary.security")
public class SecurityProperties {

    private BrowerProperties brower = new BrowerProperties();

    public BrowerProperties getBrower() {
        return brower;
    }

    public void setBrower(BrowerProperties brower) {
        this.brower = brower;
    }
}
public class BrowerProperties {
    private String loginPage = "/login_html";//默認(rèn)跳轉(zhuǎn)的接口

//    private LoginInType loginInType = LoginInType.JSON;

    public String getLoginPage() {
        return loginPage;
    }

    public void setLoginPage(String loginPage) {
        this.loginPage = loginPage;
    }

//    public LoginInType getLoginInType() {
//        return loginInType;
//    }
//
//    public void setLoginInType(LoginInType loginInType) {
//        this.loginInType = loginInType;
//    }
}

可配置的登錄頁面

#配置登錄頁面接口
#evolutionary.security.brower.loginPage = /login_html
#evolutionary.security.brower.loginInType=REDIRECT

3)默認(rèn)的登錄頁面login.html

<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>登錄 | SpringForAll - Spring Security</title>
    <link  rel="stylesheet">
</head>
<body style="background-color: #f1f1f1; padding-bottom: 0">
<div class="container" style="margin-top: 60px">
    <div class="row" style="margin-top: 100px">
        <div class="col-md-6 col-md-offset-3">
            <div class="panel panel-primary">
                <div class="panel-heading">
                    <h3 class="panel-title"><span class="glyphicon glyphicon-console"></span> Login</h3>
                </div>
                <div class="panel-body">
                    <form th:action="@{/login}" method="post">
                        <div class="form-group" style="margin-top: 30px">
                            <div class="input-group col-md-6 col-md-offset-3">
                                <div class="input-group-addon"><span class="glyphicon glyphicon-user"></span></div>
                                <input type="text" class="form-control" name="username" id="username" placeholder="賬號">
                            </div>
                        </div>
                        <div class="form-group ">
                            <div class="input-group col-md-6 col-md-offset-3">
                                <div class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></div>
                                <input type="password" class="form-control" name="password" id="password"
                                       placeholder="密碼">
                            </div>
                        </div>
                        <br>
                        <div class="form-group">
                            <div class="input-group col-md-6 col-md-offset-3 col-xs-12 ">
                                <button type="submit" class="btn btn-primary btn-block">登錄</button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

4)修改認(rèn)證邏輯的代碼見文末

6.自定義登錄成功處理

默認(rèn)情況下Spring Security登錄成功后會跳到之前引發(fā)登錄的請求噩死。修改為登錄成功返回json信息,只需實現(xiàn)AuthenticationSuccessHandler接口的onAuthenticationSuccess方法:

@Component("evolutionaryAuthenticationHandler")
public class EvolutionaryAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
 
    private Logger logger = LoggerFactory.getLogger(getClass());
 
    /**
     * spring MVC 啟動的時候會為我們注冊一個objectMapper
     */
    @Autowired
    private ObjectMapper objectMapper;
 
    @Autowired
    private SecurityProperties securityProperties;
 
    /**
     * 登錄成功會調(diào)用該方法
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登錄成功神年!");
        if (LoginInType.JSON.equals(securityProperties.getBrower().getLoginInType())) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else{
            super.onAuthenticationSuccess(request, response, authentication);
        }
 
    }
}
@Component("evolutionaryAuthenticationHandler")
public class EvolutionaryAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * spring MVC 啟動的時候會為我們注冊一個objectMapper
     */
    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 登錄成功會調(diào)用該方法
     * @param request
     * @param response
     * @param authentication
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登錄成功已维!");
        if (/*LoginInType.JSON.equals(securityProperties.getBrower().getLoginInType())*/1==1) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
        }else{
            super.onAuthenticationSuccess(request, response, authentication);
        }

    }
}

再配置登錄成功的處理方式:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()  //表單登錄
                //.loginPage("/evolutionary-loginIn.html")
                .loginPage("/logintype") //如果需要身份認(rèn)證則跳轉(zhuǎn)到這里
                .loginProcessingUrl("/login")
                .successHandler(evolutionaryAuthenticationHandler)
                .failureHandler(evolutionaryAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/logintype",securityProperties.getBrower().getLoginPage())//不校驗我們配置的登錄頁面
                .permitAll()
                .anyRequest()
                .authenticated()
                .and().csrf().disable();
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市已日,隨后出現(xiàn)的幾起案子垛耳,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堂鲜,死亡現(xiàn)場離奇詭異栈雳,居然都是意外死亡,警方通過查閱死者的電腦和手機缔莲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門哥纫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痴奏,你說我怎么就攤上這事蛀骇。” “怎么了抛虫?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵松靡,是天一觀的道長。 經(jīng)常有香客問我建椰,道長雕欺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任棉姐,我火速辦了婚禮屠列,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伞矩。我一直安慰自己笛洛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布乃坤。 她就那樣靜靜地躺著苛让,像睡著了一般。 火紅的嫁衣襯著肌膚如雪湿诊。 梳的紋絲不亂的頭發(fā)上狱杰,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天,我揣著相機與錄音厅须,去河邊找鬼仿畸。 笑死,一個胖子當(dāng)著我的面吹牛朗和,可吹牛的內(nèi)容都是我干的错沽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼眶拉,長吁一口氣:“原來是場噩夢啊……” “哼千埃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起忆植,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤镰禾,失蹤者是張志新(化名)和其女友劉穎皿曲,沒想到半個月后唱逢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吴侦,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年坞古,在試婚紗的時候發(fā)現(xiàn)自己被綠了备韧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡痪枫,死狀恐怖织堂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奶陈,我是刑警寧澤易阳,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站吃粒,受9級特大地震影響潦俺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜徐勃,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一事示、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧僻肖,春花似錦肖爵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至揉稚,卻和暖如春秒啦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窃植。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工帝蒿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巷怜。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓葛超,卻偏偏與公主長得像,于是被迫代替她去往敵國和親延塑。 傳聞我的和親對象是個殘疾皇子绣张,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,047評論 2 355

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