在SpringBoot中对SpringSecurity的基本使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35508033/article/details/79046441

参考文献:

Spring Security Architecture

What is authentication in Spring Security?

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

基本使用:

添加依赖:

<!-- 安全框架 Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

这里有一篇博客入门学习很不错:Spring boot 中 Spring Security 使用改造5部曲

我的项目中的使用:

自定义的User对象:

/**
 * 自定义的 User 对象
 * 此 User 类不是我们的数据库里的用户类,是用来安全服务的
 */
public class AnyUser extends User {
    //import org.springframework.security.core.userdetails.User;

    private Long id;

    private String nickname;

    AnyUser(
            String username,
            String password,
            Collection<? extends GrantedAuthority> authorities
    ) {
        super(username, password, authorities);
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
}
继承UserDetailsService:
首先这里我们需要重写UserDetailsService接口,然后实现该接口中的loadUserByUsername方法,通过该方法查询到对应的用户,这里之所以要实现UserDetailsService接口,是因为在Spring Security中我们配置相关参数需要UserDetailsService类型的数据。

Spring Security 支持把权限划分层次,高层次包含低层次的权限,比如`ROLE_AMDIN,ROLE_USER`两个权限,若用户拥有了ROLE_AMDIN权限,那么相当于有了ROLE_USER权限。用户被授权了ADMIN,那么就相当于有其他所有的权限。
/**
 * 自定义 UserDetailsService
 */
@Service
class AnyUserDetailsService implements UserDetailsService {

    private final UserService userService;

    public AnyUserDetailsService(UserService userService){
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        com.zhou.model.User user = userService.getByEmail(s);
        if (user == null){
            throw new UsernameNotFoundException("用户不存在");
        }
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        //对应的权限添加
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        AnyUser anyUser = new AnyUser(s, user.getPassword(), authorities);
        anyUser.setId(user.getId());
        anyUser.setNickname(user.getNickname());
        return anyUser;
    }

}
安全控制中心:

/**
 * 安全控制中心
 */
@EnableWebSecurity//@EnableWebMvcSecurity 注解开启Spring Security的功能
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;

    public WebSecurityConfig(AnyUserDetailsService userDetailsService){
        this.userDetailsService = userDetailsService;
    }

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

    /**
     * http.authorizeRequests()
     .anyRequest().authenticated()
     .and().formLogin().loginPage("/login")
     //设置默认登录成功跳转页面
     .defaultSuccessUrl("/index").failureUrl("/login?error").permitAll()
     .and()
     //开启cookie保存用户数据
     .rememberMe()
     //设置cookie有效期
     .tokenValiditySeconds(60 * 60 * 24 * 7)
     //设置cookie的私钥
     .key("")
     .and()
     .logout()
     //默认注销行为为logout,可以通过下面的方式来修改
     .logoutUrl("/custom-logout")
     //设置注销成功后跳转页面,默认是跳转到登录页面
     .logoutSuccessUrl("")
     .permitAll();
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()//authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护
                .antMatchers("/user/**","/news/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/user", true)
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and().csrf().disable();
    }

}

Spring Security提供了一个过滤器来拦截请求并验证用户身份。如果用户身份认证失败,页面就重定向到/login?error,并且页面中会展现相应的错误信息。若用户想要注销登录,可以通过访问@{/logout}请求,在完成注销之后,页面展现相应的成功消息。

自定义登录成功处理逻辑:

使登陆成功后跳到登录前页面:

//处理登录成功的。
@Component("myAuthenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        //什么都不做的话,那就直接调用父类的方法
        super.onAuthenticationSuccess(request, response, authentication);

        String url=request.getRequestURI();

        //如果是要跳转到某个页面的
        new DefaultRedirectStrategy().sendRedirect(request, response, url);

    }
}

重新配置安全中心(代码完成之后,修改配置config类代码。添加2个注解,自动注入):

@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()//authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护
                .antMatchers("/user/**","/news/**","/blog/manage/**","/blog/create/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/login")
                .successHandler(myAuthenticationSuccessHandler)//登陆成功处理
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and().csrf().disable();
    }

QQ登录实现:

准备工作:
为了方便各位测试,这里直接提供一个可以使用的:
APP ID:101386962
APP Key:2a0f820407df400b84a854d054be8b6a

提醒:因为回调地址不是 http://localhost ,所以在启动我提供的demo时,需要在host文件中添加一行:127.0.0.1 www.ictgu.cn

后端详解:

1、自定义 QQAuthenticationFilter 继承 AbstractAuthenticationProcessingFilter:
import com.alibaba.fastjson.JSON;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class QQAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private final static String CODE = "code";

    /**
     * 获取 Token 的 API
     */
    private final static String accessTokenUri = "https://graph.qq.com/oauth2.0/token";

    /**
     * grant_type 由腾讯提供
     */
    private final static String grantType = "authorization_code";

    /**
     * client_id 由腾讯提供
     */
    public static final String clientId = "101386962";

    /**
     * client_secret 由腾讯提供
     */
    private final static String clientSecret = "2a0f820407df400b84a854d054be8b6a";

    /**
     * redirect_uri 腾讯回调地址
     */
    private final static String redirectUri = "http://www.ictgu.cn/login/qq";

    /**
     * 获取 OpenID 的 API 地址
     */
    private final static String openIdUri = "https://graph.qq.com/oauth2.0/me?access_token=";

    /**
     * 获取 token 的地址拼接
     */
    private final static String TOKEN_ACCESS_API = "%s?grant_type=%s&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s";

    public QQAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl, "GET"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        String code = request.getParameter(CODE);
        String tokenAccessApi = String.format(TOKEN_ACCESS_API, accessTokenUri, grantType, clientId, clientSecret, code, redirectUri);
        QQToken qqToken = this.getToken(tokenAccessApi);
        if (qqToken != null){
            String openId = getOpenId(qqToken.getAccessToken());
            if (openId != null){
                // 生成验证 authenticationToken
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(qqToken.getAccessToken(), openId);
                // 返回验证结果
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
        return null;
    }

    private QQToken getToken(String tokenAccessApi) throws IOException{
        Document document = Jsoup.connect(tokenAccessApi).get();
        String tokenResult = document.text();
        String[] results = tokenResult.split("&");
        if (results.length == 3){
            QQToken qqToken = new QQToken();
            String accessToken = results[0].replace("access_token=", "");
            int expiresIn = Integer.valueOf(results[1].replace("expires_in=", ""));
            String refreshToken = results[2].replace("refresh_token=", "");
            qqToken.setAccessToken(accessToken);
            qqToken.setExpiresIn(expiresIn);
            qqToken.setRefresh_token(refreshToken);
            return qqToken;
        }
        return null;
    }

    private String getOpenId(String accessToken) throws IOException{
        String url = openIdUri + accessToken;
        Document document = Jsoup.connect(url).get();
        String resultText = document.text();
        Matcher matcher = Pattern.compile("\"openid\":\"(.*?)\"").matcher(resultText);
        if (matcher.find()){
            return matcher.group(1);
        }
        return null;
    }

    class QQToken {

        /**
         * token
         */
        private String accessToken;

        /**
         * 有效期
         */
        private int expiresIn;

        /**
         * 刷新时用的 token
         */
        private String refresh_token;

        String getAccessToken() {
            return accessToken;
        }

        void setAccessToken(String accessToken) {
            this.accessToken = accessToken;
        }

        public int getExpiresIn() {
            return expiresIn;
        }

        void setExpiresIn(int expiresIn) {
            this.expiresIn = expiresIn;
        }

        public String getRefresh_token() {
            return refresh_token;
        }

        void setRefresh_token(String refresh_token) {
            this.refresh_token = refresh_token;
        }
    }
}
说明:Filter 过滤时执行的方法是 doFilter(),由于 QQAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter,所以过滤时使用的是父类的doFilter() 方法。
说明:doFilter()方法中,有一步是 attemptAuthentication(request, response) 即为 QQAuthenticationFilter 中实现的方法。这个方法中调用了 this.getAuthenticationManager().authenticate(authRequest),这里自定义了类 QQAuthenticationManager,代码如下:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zhou.model.User;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static com.zhou.config.qq.QQAuthenticationFilter.clientId;

public class QQAuthenticationManager implements AuthenticationManager {
    private static final List<GrantedAuthority> AUTHORITIES = new ArrayList<>();

    /**
     * 获取 QQ 登录信息的 API 地址
     */
    private final static String userInfoUri = "https://graph.qq.com/user/get_user_info";

    /**
     * 获取 QQ 用户信息的地址拼接
     */
    private final static String USER_INFO_API = "%s?access_token=%s&oauth_consumer_key=%s&openid=%s";

    static {
        AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {
        if (auth.getName() != null && auth.getCredentials() != null) {
            User user = null;
            try {
                user = getUserInfo(auth.getName(), (String) (auth.getCredentials()));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return new UsernamePasswordAuthenticationToken(user,
                    null, AUTHORITIES);
        }
        throw new BadCredentialsException("Bad Credentials");
    }

    private User getUserInfo(String accessToken, String openId) throws Exception {
        String url = String.format(USER_INFO_API, userInfoUri, accessToken, clientId, openId);
        Document document;
        try {
            document = Jsoup.connect(url).get();
        } catch (IOException e) {
            throw new BadCredentialsException("Bad Credentials!");
        }
        String resultText = document.text();
        JSONObject json = JSON.parseObject(resultText);

        User user = new User();
        user.setNickname(json.getString("nickname"));
        user.setEmail("暂无。。。。");
        //user.setGender(json.getString("gender"));
        //user.setProvince(json.getString("province"));
        //user.setYear(json.getString("year"));
        user.setAvatar(json.getString("figureurl_qq_2"));

        return user;
    }
说明:QQAuthenticationManager 的作用是通过传来的 token 和 openID 去请求腾讯的getUserInfo接口,获取腾讯用户的信息,并生成新的 Authtication 对象。
接下来就是要将 QQAuthenticationFilter 与 QQAuthenticationManager 结合,配置到 Spring Security 的过滤器链中。代码如下:
@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()//authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护
                .antMatchers("/user/**","/news/**","/blog/manage/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/login")
                .successHandler(myAuthenticationSuccessHandler)//登陆成功处理
                .permitAll()
                .and()
                .logout()
                .permitAll()
                .and().csrf().disable();
        // 在 UsernamePasswordAuthenticationFilter 前添加 QQAuthenticationFilter
        http.addFilterAt(qqAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 自定义 QQ登录 过滤器
     */
    private QQAuthenticationFilter qqAuthenticationFilter(){
        QQAuthenticationFilter authenticationFilter = new QQAuthenticationFilter("/login/qq");
        //SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
        //successHandler.setAlwaysUseDefaultTargetUrl(true);
        //successHandler.setDefaultTargetUrl("/user");
        MyAuthenticationSuccessHandler successHandler = new MyAuthenticationSuccessHandler();
        authenticationFilter.setAuthenticationManager(new QQAuthenticationManager());
        authenticationFilter.setAuthenticationSuccessHandler(successHandler);
        return authenticationFilter;
    }
说明:由于腾讯的回调地址是 /login/qq,所以 QQAuthenticationFilter 拦截的路径是 /login/qq,然后将 QQAuthenticationFilter 置于 UsernamePasswordAuthenticationFilter 相同级别的位置。

前端说明:

前端很简单,一个QQ登陆按钮,代码如下:
<a href="https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101386962&redirect_uri=http://www.ictgu.cn/login/qq&state=test" class="btn btn-primary btn-block">QQ登录</a>

其他说明:
腾讯官网原话:openid是此网站上唯一对应用户身份的标识,网站可将此ID进行存储便于用户下次登录时辨识其身份,或将其与用户在网站上的原有账号进行绑定。
通过QQ登录获取的 openid 用于与自己网站的账号一一对应。
展开阅读全文

没有更多推荐了,返回首页