美文网首页
搭建一个自己的平台的心理路程之鉴权(二)

搭建一个自己的平台的心理路程之鉴权(二)

作者: 东南枝下 | 来源:发表于2020-06-14 15:32 被阅读0次

为了实现在登录界面登录后,把token写入cookie的效果,需要配置successHandler,继承SavedRequestAwareAuthenticationSuccessHandler在页面登录成功后生成token
上一话生成的是jwt,但在successHandler中生成的token不是jwtToken(不知道该怎么生成才好),而且生成的token无法鉴权成功,猜测是无法读取到身份信息,所以改用RedisTokenStore。

服务端

绘制一个登录界面

1、绘制界面使用的是bootstrap,也用到了jquery,密码使用md5进行初步加密,所以导入以下依赖

图片.png

2、使用thymeleaf,导入依赖


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

spring:
    thymeleaf:
    cache: false #关闭缓存

3、绘制登录界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Signin Template for Bootstrap</title>
    <link href="assets/css/bootstrap.min.css" rel="stylesheet">
    <link href="assets/css/signin.css" rel="stylesheet">
    <script src="assets/js/jquery-3.5.1.min.js"></script>
    <script src="assets/js/bootstrap.min.js"></script>
    <script src="assets/js/md5.js"></script>
</head>
<body class="text-center">
<!--<form class="form-signin" action="/form-login" method="post">-->
<div class="form-signin">
    <img class="mb-4" src="assets/img/jzo.png" alt="" width="210" height="72">
    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label class="sr-only">Username</label>
    <input type="text" name="username" class="form-control" placeholder="Username"
           required="" autofocus="">
    <label class="sr-only">Password</label>
    <input type="password" name="password" class="form-control" placeholder="Password"
           required="">
    <input type="hidden" name="scope" value="all"
           required="" autofocus="">
    <input type="hidden" name="grant_type" value="password"
           required="" autofocus="">
    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" value="remember-me"/> Remember me
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit" onclick="login()">Sign in</button>
    <p class="mt-5 mb-3 text-muted">© jenson</p>
</div>
<script type="text/javascript">
    function login() {
        $.ajax({
            type: "POST",
            url: "/form-login",
            data: {
                username: $("input[name='username']").val(),
                password: hex_md5($("input[name='password']").val()),
                scope: $("input[name='scope']").val(),
                grant_type: $("input[name='grant_type']").val()
            },
            dataType: "json",
            headers: {"Authorization": "Basic anplcm8tY2xpZW50Omp6ZXJvLXNlY3JldA=="},
            async: false
            // success: function (data) {
            // }
        });
    }
</script>
</body>
</html>

为了调用接口时带上headers,采用ajax。

4、编写Controller


@Controller
public class LoginController {

    @GetMapping("/login")
    public String login(@RequestParam(value = "error", required = false) String error, Model model){
        if (error != null) {
            return "loginFailure";
        }

        return "login";
    }
}

5、在WebSecurity中配置登录页,完整代码如下

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

    @Autowired
    private BaseUserDetailsService baseUserDetailsService;

    @Autowired
    private UserAuthenticationSuccessHandler userAuthenticationSuccessHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 允许匿名访问所有接口 主要是 oauth 接口
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//      http.authorizeRequests()
//              .antMatchers("/**").permitAll();

        http
                // 配置登陆页/login并允许访问
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/form-login")
                .defaultSuccessUrl("/index")
                .successHandler(userAuthenticationSuccessHandler)
                .failureUrl("/login?error=error")
                .permitAll()
                // 登出页
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                // 其余所有请求全部需要鉴权认证
                .and().authorizeRequests().antMatchers("/**").permitAll()
                .and().csrf().disable();

    }

    /**
     * 用户验证
     * @param auth
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(baseUserDetailsService).passwordEncoder(passwordEncoder());
    }


}


6、登录页面如下

图片.png

RedisToken 配置及登录成功后successHandler实现

1、redis数据库依赖和配置


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        
spring:
    redis:
    host: redis.jzero.org
    port: 6379
    database: 1

2、创建 redisTokenStore 的bean


@Configuration
public class RedisTokenConfig {
    @Bean
    public TokenStore redisTokenStore(RedisConnectionFactory redisConnectionFactory) {
        return new RedisTokenStore(redisConnectionFactory);
    }
}

3、 实现UserDetailsService,从数据库中获取密码,此处使用的是jpa来实现数据库操作

@Slf4j
@Component
public class BaseUserDetailsService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username is:" + username);
        // 用户角色也应在数据库中获取
        String role;
        String password;
        if ("admin".equals(username)) {
            // 特殊账号
            role = "ROLE_ADMIN";
            // 线上环境应该通过用户名查询数据库获取加密后的密码
            password = passwordEncoder.encode(DigestUtils.md5DigestAsHex("123456".getBytes()));
            System.out.println("password:" + password);
        } else {
            // 从数据库中获取密码
            role = "ROLE_USER";
            org.jzero.oauth.domain.entity.User user = userRepository.findByUsername(username);
            password = user.getPassword();

        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(role));

        return new User(username, password, authorities);
    }
}

4、在 AuthorizationServerConfigurer 中使用 redisTokenStore ,完整代码如下

@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
    @Autowired
    public PasswordEncoder passwordEncoder;

    @Autowired
    public UserDetailsService baseUserDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore redisTokenStore;

    @Autowired
    private AuthorizationServerTokenServices redisTokenService;

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 集成websecurity认证
        endpoints.authenticationManager(authenticationManager);
        endpoints.tokenServices(redisTokenService);
        // 注册redis令牌仓库
        endpoints.tokenStore(redisTokenStore);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("jzero-client")
                .secret(passwordEncoder.encode("jzero-secret"))
//              .authorizedGrantTypes("refresh_token", "authorization_code", "password")
                .authorizedGrantTypes("password")
//              .accessTokenValiditySeconds(3600)
                .scopes("all");
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
        security.checkTokenAccess("isAuthenticated()");
        security.tokenKeyAccess("permitAll()");
    }
}

5、为了在successHandler中生成token,需要自己实现TokenService

@Configuration
public class TokenServicesConfig {
    @Autowired
    private TokenStore redisTokenStore;

    @Bean
    public AuthorizationServerTokenServices redisTokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setTokenStore(redisTokenStore);
        service.setAccessTokenValiditySeconds(600);
        service.setRefreshTokenValiditySeconds(7200);
        return service;
    }
}

6、进入正题,继承SavedRequestAwareAuthenticationSuccessHandler,编写successHandler生成token

@Slf4j
@Component
public class UserAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthorizationServerTokenServices redisTokenService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        String header = request.getHeader("Authorization");
        String username = authentication.getName();

        if (header == null || !header.toLowerCase().startsWith("basic ")) {
            throw new UnapprovedClientAuthenticationException("请求头中无client信息");
        }
        String[] tokens = extractAndDecodeHeader(header, request);
        assert tokens.length == 2;


        String clientId = tokens[0];
        String clientSecret = tokens[1];

        //拿到了clientDetails
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
        //校验
        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("client-id对应信息不存在:" + clientId);
        } else if (!new BCryptPasswordEncoder().matches(clientSecret, clientDetails.getClientSecret())) {
            throw new UnapprovedClientAuthenticationException("client-secret对应信息不匹配:" + clientSecret);
        }

        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "password");

        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

        OAuth2AccessToken token = redisTokenService.createAccessToken(oAuth2Authentication);

//      jwtTokenStore.storeAccessToken(token, oAuth2Authentication);

        // 将session 写入cookie
        Cookie cookie = new Cookie("access_token", token.getValue());
//      cookie.setMaxAge(60);             //存活一分钟
        cookie.setMaxAge(60 * 60);          //存活一小时
//      cookie.setMaxAge(24*60*60);       //存活一天
//      cookie.setMaxAge(365*24*60*60);     //存活一年
        response.addCookie(cookie);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(token));
        System.out.println("ok");
    }

    /**
     * Decodes the header into a username and password.
     *
     * @throws BadCredentialsException if the Basic header is not present or is not valid
     *                                 Base64
     */
    private String[] extractAndDecodeHeader(String header, HttpServletRequest request)
            throws IOException {

        byte[] base64Token = header.substring(6).getBytes("UTF-8");
        byte[] decoded;
        try {
            decoded = Base64.getDecoder().decode(base64Token);
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException(
                    "Failed to decode basic authentication token");
        }

//          String token = new String(decoded, getCredentialsCharset(request));
        String token = new String(decoded, "UTF-8");

        int delim = token.indexOf(":");

        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[]{token.substring(0, delim), token.substring(delim + 1)};
    }
}

参考

参考文档:https://segmentfault.com/a/1190000016583573?utm_source=tag-newest

相关文章

网友评论

      本文标题:搭建一个自己的平台的心理路程之鉴权(二)

      本文链接:https://www.haomeiwen.com/subject/vuvxxktx.html